commit 90d4979b5d9441fc45ae2d460fee8afcb6b234d2 Author: noschmarrn Date: Sat Mar 7 18:43:03 2026 +0000 release: BreznGEO v1.0.0 — initial release Complete rebrand from Bavarian Rank Engine to BreznGEO. Fresh start at v1.0.0 with clean plugin identity. Co-Authored-By: Claude Sonnet 4.6 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..dd84ea7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..7331ef1 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,32 @@ +name: Release + +on: + push: + tags: + - 'v*.*.*' + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Get version from tag + id: version + run: | + VERSION="${GITHUB_REF#refs/tags/v}" + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + + - name: Create ZIP + run: zip -r bavarian-rank-engine.zip bavarian-rank-engine/ + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + name: "Bavarian Rank Engine v${{ steps.version.outputs.version }}" + files: bavarian-rank-engine.zip + generate_release_notes: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fb0fc78 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +vendor/ +.phpunit.result.cache +firebase-debug.log +composer.phar +*.zip +/node_modules/ +.claude/ +*.log diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/README.de.md b/README.de.md new file mode 100644 index 0000000..eb04701 --- /dev/null +++ b/README.de.md @@ -0,0 +1,391 @@ +# Bavarian Rank Engine + +![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.3.5-orange) +![Tests](https://img.shields.io/badge/Tests-112%20passing-brightgreen) + +🇬🇧 [English version → README.md](README.md) + +**Website:** [bavarianrankengine.com](https://bavarianrankengine.com)  ·  [How To](https://bavarianrankengine.com/howto.html)  ·  [FAQ](https://bavarianrankengine.com/faq.html)  ·  [Changelog](https://bavarianrankengine.com/changelog.html) + +--- + +Bavarian Rank Engine ist ein schlankes SEO- & GEO-Plugin für WordPress. Es generiert KI-Metabeschreibungen, gibt Schema.org-Strukturdaten aus, erstellt GEO-Inhaltsblöcke für KI-Engines und verwaltet den Crawler-Zugriff über robots.txt und llms.txt — alles in einem Plugin, ohne dass etwas hinter einer Paywall versteckt wird. + +Es funktioniert mit oder ohne KI-Key. Es integriert sich ohne Konflikte in Rank Math, Yoast, AIOSEO und SEOPress. Kein SaaS. Keine Telemetrie. Keine Upsells. + +--- + +## Warum dieses Plugin existiert + +Die meisten WordPress-SEO-Plugins haben sich in die gleiche Richtung entwickelt: aufgeblähte Feature-Sets, Dashboards voller Metriken, die niemand gebraucht hat, und ein Preismodell, das die nützlichen Funktionen hinter einem monatlichen Abo versteckt. + +Die KI-Welle hat es schlimmer gemacht. Plugins fingen an, „KI-gestützte" Features anzubieten — aber als Proxy-Dienst. Man zahlt eine monatliche Gebühr, die Inhalte werden über deren Server geleitet, sie rufen die KI-API im eigenen Namen auf und schlagen eine Marge drauf. + +BRE verfolgt einen anderen Ansatz: + +- **Direkter API-Zugriff.** Du hinterlegst deinen eigenen Key von OpenAI, Anthropic, Google oder xAI. BRE ruft die API direkt auf. Kein Mittelsmann, keine Marge, keine Daten über Server Dritter. +- **Klarer Output, kein Lärm.** Metabeschreibungen, Strukturdaten, KI-Inhaltsblöcke für GEO, Bot-Steuerung. Keine Lesbarkeits-Scores, keine Keyword-Dichte-Meter, keine Upsell-Banner. +- **Keine Subscription.** GPL-2.0. Kostenlos auf beliebig vielen Sites nutzbar. Die einzigen Kosten sind die API-Nutzung — typischerweise Bruchteile eines Cents pro Beitrag. +- **Keine Telemetrie.** BRE sendet keine Daten nach Hause. Kein Usage-Tracking, kein Remote-Logging, keine Analytics, die den eigenen Server verlassen. +- **Funktioniert ohne KI.** Kein API-Key? Der Fallback-Extraktor erzeugt eine brauchbare Metabeschreibung aus dem Artikelinhalt per Satzgrenzenerkennung. Jeder Beitrag bekommt eine Beschreibung. + +Entwickelt in Passau, Bayern — für [Donau2Space](https://donau2space.de), einen persönlichen KI-Blog, für den ich genau das gebraucht habe — und nichts mehr. + +--- + +## Inhaltsverzeichnis + +- [Warum dieses Plugin existiert](#warum-dieses-plugin-existiert) +- [Verzeichnisstruktur](#verzeichnisstruktur) +- [Features](#features) +- [Datenspeicherung](#datenspeicherung) +- [Sicherheit](#sicherheit) +- [KI-Provider](#ki-provider) +- [Hooks & Erweiterbarkeit](#hooks--erweiterbarkeit) +- [AJAX-Schnittstellen](#ajax-schnittstellen) +- [Installation](#installation) +- [Technischer Stack](#technischer-stack) +- [Lizenz](#lizenz) + +--- + +## Verzeichnisstruktur + +``` +bavarian-rank-engine/ +├── bavarian-rank-engine.php # Plugin-Header, Konstanten (BRE_VERSION, BRE_DIR, BRE_URL) +├── uninstall.php # Aufräumen bei Plugin-Löschung +├── assets/ +│ ├── admin.css # Gemeinsames Admin-Stylesheet +│ ├── admin.js # Provider-Selektor, Verbindungstest +│ ├── bulk.js # Bulk-Generator AJAX-Loop + Progress-UI +│ ├── editor-meta.js # Meta Editor Box: Live-Zähler, KI-Regen-Button +│ ├── geo-editor.js # GEO Block Editor: Generieren / Löschen Button +│ ├── geo-frontend.css # Minimales Stylesheet für .bre-geo auf dem Frontend +│ ├── link-suggest.js # Interne Link-Vorschläge: Trigger, UI, Apply (Gutenberg + Classic) +│ └── seo-widget.js # SEO Analyse Widget: Live-Auswertung im Editor +├── includes/ +│ ├── Core.php # Singleton-Bootstrap, lädt alle Abhängigkeiten +│ ├── Admin/ +│ │ ├── AdminMenu.php # Menüstruktur + Dashboard-Render +│ │ ├── BulkPage.php # Bulk Generator Admin-Seite +│ │ ├── GeoEditorBox.php # GEO Block Meta-Box im Post-Editor +│ │ ├── GeoPage.php # GEO Block Einstellungsseite +│ │ ├── LinkAnalysis.php # AJAX-Handler für Link-Analyse Dashboard +│ │ ├── LinkSuggestPage.php # Einstellungsseite für interne Link-Vorschläge +│ │ ├── MetaEditorBox.php # Meta Description Meta-Box im Post-Editor +│ │ ├── MetaPage.php # Meta Generator Einstellungsseite +│ │ ├── ProviderPage.php # AI Provider Einstellungsseite +│ │ ├── SchemaMetaBox.php # Schema.org per-Post Meta-Box +│ │ ├── TxtPage.php # TXT-Dateien-Seite: llms.txt + robots.txt (Tabs) +│ │ ├── SchemaPage.php # Schema.org Einstellungsseite +│ │ ├── SeoWidget.php # SEO Analyse Sidebar Widget +│ │ ├── SettingsPage.php # Zentrales getSettings() — mergt alle Option-Keys +│ │ └── views/ # PHP-Templates für alle Admin-Seiten +│ ├── Features/ +│ │ ├── CrawlerLog.php # KI-Bot-Besuche loggen (eigene DB-Tabelle) +│ │ ├── GeoBlock.php # GEO Quick Overview Block (Frontend-Ausgabe) +│ │ ├── LlmsTxt.php # /llms.txt Endpunkt mit ETag/Cache +│ │ ├── LinkSuggest.php # Interne Link-Vorschläge: Matching-Engine + AJAX-Handler + Meta-Box +│ │ ├── MetaGenerator.php # Kernlogik: KI-Aufruf, Speichern, Bulk, AJAX +│ │ ├── RobotsTxt.php # robots.txt Bot-Blocking via WP-Filter +│ │ └── SchemaEnhancer.php # JSON-LD Schema.org Ausgabe in wp_head +│ ├── Helpers/ +│ │ ├── BulkQueue.php # Mutex-Lock für Bulk-Prozesse (Transient-basiert) +│ │ ├── FallbackMeta.php # Meta-Extraktion aus Post-Content ohne KI +│ │ ├── KeyVault.php # API-Key Verschleierung vor dem Schreiben in die DB +│ │ └── TokenEstimator.php # Grobe Token-Schätzung für Kostenvorschau im Bulk +│ └── Providers/ +│ ├── ProviderInterface.php # Interface: getId, getName, getModels, testConnection, generateText +│ ├── ProviderRegistry.php # Registry-Pattern: Provider registrieren und abrufen +│ ├── AnthropicProvider.php # Claude API (Messages API) +│ ├── GeminiProvider.php # Google Gemini (generateContent API) +│ ├── GrokProvider.php # xAI Grok (OpenAI-kompatibler Endpunkt) +│ └── OpenAIProvider.php # OpenAI GPT (Chat Completions API) +└── vendor/ # Composer-Abhängigkeiten (nur Produktionsstand) +``` + +--- + +## Features + +### AI Meta Generator + +Generiert SEO-optimierte Meta-Beschreibungen (150–160 Zeichen) automatisch beim Veröffentlichen eines Beitrags. Der Prompt ist vollständig anpassbar; unterstützte Platzhalter: `{title}`, `{content}`, `{excerpt}`, `{language}`. + +**Spracherkennung:** Die Zielsprache wird automatisch aus Polylang, WPML oder dem WordPress-Locale ermittelt und im Prompt übergeben — ohne Konfiguration. + +**SEO-Plugin-Integration:** Generierte Beschreibungen landen nicht nur in `_bre_meta_description`, sondern auch direkt im nativen Feld des aktiven SEO-Plugins: + +| SEO-Plugin | Meta-Feld | +|---|---| +| Rank Math | `rank_math_description` | +| Yoast SEO | `_yoast_wpseo_metadesc` | +| AIOSEO | `_aioseo_description` | +| SEOPress | `_seopress_titles_desc` | +| (keins aktiv) | BRE gibt `` selbst aus | + +**Token-Modus:** Wahlweise wird der gesamte Artikelinhalt gesendet (`full`) oder auf eine konfigurierbare Token-Anzahl (100–8000) gekürzt (`limit`). Das Kürzen erfolgt über `TokenEstimator` — eine wortbasierte Schätzung ohne externe Bibliothek. + +**Fallback ohne KI:** `FallbackMeta::extract()` liefert immer eine brauchbare Beschreibung — auch ohne API-Key oder bei Fehlern. Vollständig multibyte-safe via `mb_substr` / `mb_strrpos`. + +--- + +### GEO Block (Quick Overview) + +Generiert KI-gestützte Inhaltsblöcke direkt im Artikeltext für Generative Engine Optimization: + +- **Summary** — Kurzüberblick des Artikels +- **Key Points** — Stichpunktliste der wichtigsten Aussagen +- **FAQ** — Frage-Antwort-Paare (nur ab konfiguriertem Wort-Schwellenwert, Standard: 350 Wörter) + +**Einfügeposition** (konfigurierbar): nach dem ersten Absatz (Standard), oben, unten. + +**Ausgabe-Modi:** + +| Modus | Verhalten | +|---|---| +| `details_collapsible` | Natives HTML `
` — zugeklappt, kein JavaScript nötig | +| `open_always` | Block immer sichtbar | +| `store_only_no_frontend` | Nur in DB speichern, keine Frontend-Ausgabe (z.B. für FAQPage-Schema) | + +Alle Labels, Akzentfarbe, Farbschema (Auto/Hell/Dunkel) und Custom CSS sind über die Admin-Seite konfigurierbar — ohne Code. + +--- + +### Schema.org Enhancer + +Gibt JSON-LD-Strukturdaten und Meta-Tags in `` aus. Einstellungen unter **Bavarian Rank → Schema.org**. Jeder Typ ist einzeln aktivierbar: + +| Typ | Schema.org-Type | Hinweis | +|---|---|---| +| `organization` | `Organization` | Name, URL, Logo, `sameAs`-Links | +| `author` | `Person` | Autorenname, Profil-URL, optionaler Twitter-`sameAs` | +| `speakable` | `WebPage` + `SpeakableSpecification` | CSS-Selektoren auf H1 und ersten Absatz | +| `article_about` | `Article` | Headline, Publish/Modified, Description, Publisher | +| `breadcrumb` | `BreadcrumbList` | Automatisch unterdrückt wenn Rank Math oder Yoast aktiv | +| `ai_meta_tags` | — | `` mit `max-snippet:-1` | +| `faq_schema` | `FAQPage` | Automatisch aus GEO Block Daten befüllt | +| `blog_posting` | `BlogPosting` / `Article` | Mit eingebettetem `author` und Featured Image | +| `image_object` | `ImageObject` | Featured Image mit Dimensionen und Caption | +| `video_object` | `VideoObject` | YouTube/Vimeo wird automatisch erkannt | +| `howto` | `HowTo` | Schrittweise Anleitung — Daten per Metabox | +| `review` | `Review` | Bewertung mit `ratingValue` — Daten per Metabox | +| `recipe` | `Recipe` | Zutaten, Zeiten, Nährwerte — Daten per Metabox | +| `event` | `Event` | Datum, Ort, Veranstalter — Daten per Metabox | + +--- + +### llms.txt + +Bedient `/llms.txt` und paginierte Folgedateien über einen `parse_request`-Hook mit Priorität 1 — vor WordPress-Routing, kein Rewrite-Rule-Flush nötig. + +**HTTP-Caching:** ETag, Last-Modified, Cache-Control. Transient-Cache wird bei jeder Einstellungsänderung automatisch invalidiert. + +**Rank Math Konfliktwarnung:** Falls Rank Math ebenfalls eine llms.txt ausliefern will, zeigt BRE einen Admin-Hinweis an — BRE hat wegen Priorität 1 automatisch Vorrang. + +--- + +### robots.txt Manager + +Hängt `Disallow`-Blöcke über den WordPress-Filter `robots_txt` an — die WordPress-eigene robots.txt bleibt erhalten. 13 KI-Bots einzeln steuerbar: GPTBot, ClaudeBot, Google-Extended, PerplexityBot, CCBot, Applebot-Extended, Bytespider, DataForSeoBot, ImagesiftBot, omgili, Diffbot, FacebookBot, Amazonbot. + +--- + +### Bulk Generator + +Batch-Verarbeitung aller veröffentlichten Beiträge ohne Meta-Beschreibung. Läuft als AJAX-Request im Browser — kein WP-Cron, keine CLI nötig. 1–20 Beiträge pro Batch, 6s Delay, bis zu 3 Versuche je Post, Mutex-Lock via Transient. + +--- + +### Crawler Log + +Loggt Besuche bekannter KI-Bots in der Tabelle `{prefix}bre_crawler_log` (bot_name, ip_hash SHA-256, url, visited_at). Einträge älter als 90 Tage werden automatisch bereinigt. Dashboard zeigt 30-Tage-Zusammenfassung. + +--- + +## Datenspeicherung + +### WordPress Options (wp_options) + +| Option-Key | Inhalt | +|---|---| +| `bre_settings` | Aktiver Provider, API-Keys (verschleiert), Modell-Auswahl, Token-Kosten, `ai_enabled`-Flag | +| `bre_meta_settings` | Meta Generator: Auto-Modus, Post-Types, Token-Modus, Prompt | +| `bre_schema_settings` | Schema.org: aktivierte Typen, Organization sameAs-URLs | +| `bre_geo_settings` | GEO Block: Modus, Position, Labels, CSS, Prompt, Farbschema | +| `bre_robots_settings` | robots.txt: blockierte Bots | +| `bre_llms_settings` | llms.txt: Titel, Beschreibung, Featured-Links, Footer, Seitenanzahl | +| `bre_usage_stats` | Akkumulierte Token-Nutzung: `tokens_in`, `tokens_out`, `count` | +| `bre_first_activated` | Unix-Timestamp der Erstaktivierung (für Welcome Notice) | + +### Post Meta (wp_postmeta) + +| Meta-Key | Inhalt | +|---|---| +| `_bre_meta_description` | Generierte Meta-Beschreibung | +| `_bre_meta_source` | Quelle: `ai` / `fallback` / `manual` | +| `_bre_bulk_failed` | Letzter Fehler beim Bulk-Versuch | +| `_bre_geo_summary` | GEO Block Summary | +| `_bre_geo_bullets` | GEO Block Key Points (JSON-Array) | +| `_bre_geo_faq` | GEO Block FAQ (JSON-Array) | + +### Transients + +| Transient | TTL | Zweck | +|---|---|---| +| `bre_llms_cache_{n}` | 1 Stunde | Gecachter llms.txt Inhalt je Seite | +| `bre_link_analysis` | 1 Stunde | Dashboard Link-Analyse Ergebnis | +| `bre_bulk_running` | 15 Minuten | Mutex-Lock für den Bulk Generator | +| `bre_meta_stats` | 5 Minuten | Dashboard Meta-Coverage-Abfrage | +| `bre_crawler_summary` | 5 Minuten | Dashboard Crawler-Zusammenfassung (letzte 30 Tage) | + +> **Uninstall:** `uninstall.php` löscht `bre_settings` und `_bre_meta_description` für alle Posts. Die übrigen Option-Keys und die `bre_crawler_log`-Tabelle müssen manuell gelöscht werden. + +--- + +## Sicherheit + +### API-Key Verschleierung (KeyVault) + +``` +Klartextkey → XOR(key, sha256(AUTH_KEY . SECURE_AUTH_KEY)) → base64 → "bre1:" +``` + +Kein `openssl_*` oder externe Extension nötig — läuft auf jeder PHP 8.0+ Installation. Das Präfix `bre1:` ermöglicht spätere Migration ohne Breaking Change. + +**Sicherheitsgrenzen:** XOR mit statischem Salt ist Verschleierung, keine kryptografische Verschlüsselung. Für maximale Sicherheit können Keys als `wp-config.php`-Konstanten definiert werden: + +```php +define( 'BRE_OPENAI_KEY', 'sk-...' ); +define( 'BRE_ANTHROPIC_KEY', 'sk-ant-...' ); +define( 'BRE_GEMINI_KEY', 'AI...' ); +define( 'BRE_GROK_KEY', 'xai-...' ); +``` + +### CSRF-Schutz und Capability Checks + +Jeder AJAX-Handler ohne Ausnahme: + +```php +check_ajax_referer( 'bre_admin', 'nonce' ); +if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( 'Unauthorized', 403 ); +} +``` + +Kein `wp_ajax_nopriv_`-Handler — alle Endpunkte erfordern `manage_options`. + +### CSS-Sanitierung (GEO Block) + +Das Custom-CSS-Feld des GEO-Blocks wird durch `Helpers\Css::sanitize_declarations()` bereinigt — entfernt Kommentare, geschweifte Klammern, At-Regeln (`@import`, `@media` usw.), `url()`, `expression()` und `javascript:`, bevor die Ausgabe über `wp_add_inline_style()` injiziert wird. + +### Datenschutz (DSGVO) + +CrawlerLog speichert IPs ausschließlich als SHA-256-Hash. Originalwert wird nie persistiert. Einträge nach 90 Tagen automatisch gelöscht. + +--- + +## KI-Provider + +| Provider | Klasse | API-Basis-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` | + +Neuen Provider hinzufügen: `ProviderInterface` implementieren, in `Core.php` via `$registry->register()` eintragen — erscheint automatisch in allen Dropdowns. + +--- + +## Hooks & Erweiterbarkeit + +### `bre_prompt` (Filter) + +```php +add_filter( 'bre_prompt', function( string $prompt, WP_Post $post ): string { + $keyword = get_post_meta( $post->ID, 'focus_keyword', true ); + return $keyword ? $prompt . "\nFokus-Keyword: {$keyword}" : $prompt; +}, 10, 2 ); +``` + +### `bre_meta_saved` (Action) + +```php +add_action( 'bre_meta_saved', function( int $post_id, string $description ): void { + my_cdn_purge( get_permalink( $post_id ) ); +}, 10, 2 ); +``` + +--- + +## AJAX-Schnittstellen + +Alle Endpunkte erfordern `manage_options` (kein `nopriv`). + +| Action | Handler | Beschreibung | +|---|---|---| +| `bre_regen_meta` | `MetaEditorBox::ajax_regen` | Meta-Beschreibung für einzelnen Post neu generieren | +| `bre_test_connection` | `ProviderPage::ajax_test_connection` | API-Key und Verbindung testen | +| `bre_get_default_prompt` | `ProviderPage::ajax_get_default_prompt` | Standard-Prompt zurücksetzen | +| `bre_link_analysis` | `LinkAnalysis::ajax_analyse` | Link-Analyse ausführen | +| `bre_link_suggestions` | `LinkSuggest::ajax_suggest` | Top-10 interne Link-Vorschläge für aktuellen Beitrag zurückgeben | +| `bre_geo_generate` | `GeoEditorBox::ajax_generate` | GEO Block generieren | +| `bre_geo_clear` | `GeoEditorBox::ajax_clear` | GEO Block löschen | +| `bre_llms_clear_cache` | `TxtPage::ajax_clear_cache` | llms.txt Cache leeren | +| `bre_dismiss_llms_notice` | `LlmsTxt::ajax_dismiss_notice` | Rank-Math-Hinweis ausblenden | +| `bre_dismiss_welcome` | `AdminMenu::ajax_dismiss_welcome` | Welcome Notice per User ausblenden | +| `bre_bulk_generate` | `MetaGenerator::ajaxBulkGenerate` | Nächsten Batch verarbeiten | +| `bre_bulk_stats` | `MetaGenerator::ajaxBulkStats` | Fortschritt abrufen | +| `bre_bulk_release` | `MetaGenerator::ajaxBulkRelease` | Mutex-Lock manuell freigeben | +| `bre_bulk_status` | `MetaGenerator::ajaxBulkStatus` | Lock-Status prüfen | + +--- + +## Installation + +**Via GitHub Release (empfohlen):** +1. `bavarian-rank-engine.zip` vom [neuesten Release](https://github.com/noschmarrn/bavarianrankengine/releases/latest) herunterladen +2. In WordPress unter *Plugins → Installieren → Plugin hochladen* einspielen + +**Manuell (clone):** +```bash +cd /path/to/wordpress/wp-content/plugins/ +git clone https://github.com/noschmarrn/bavarianrankengine.git bavarian-rank-engine +wp plugin activate bavarian-rank-engine +``` + +**Nach der Aktivierung:** +1. *Bavarian Rank → AI Provider* — Provider wählen, API-Key hinterlegen, Verbindungstest +2. *Meta Generator* — Auto-Modus aktivieren, Post-Types auswählen + +Kein JavaScript-Build-Step. Alle Assets unter `assets/` sind direkte JS/CSS-Dateien. + +--- + +## Technischer Stack + +| Komponente | Technologie | +|---|---| +| Backend | PHP 8.0+, WordPress Plugin API | +| Namespace | `BavarianRankEngine\` | +| Architektur | Singleton-Core, Registry-Pattern (Provider), Feature-Klassen mit `register()` | +| Datenbank | WordPress Options API, `wpdb` (eigene Tabelle für CrawlerLog) | +| Caching | WordPress Transients | +| Frontend | Vanilla JS + jQuery (WordPress-integriert), kein Build-Step | +| I18n | `.pot`-File, Text-Domain `bavarian-rank-engine` | +| Tests | PHPUnit (102 Tests, 216 Assertions) | +| Coding Standard | WordPress PHPCS | +| Lizenz | GPL-2.0-or-later | + +--- + +## Lizenz + +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) diff --git a/README.md b/README.md new file mode 100644 index 0000000..35d501d --- /dev/null +++ b/README.md @@ -0,0 +1,546 @@ +# Bavarian Rank Engine + +![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.3.5-orange) +![Tests](https://img.shields.io/badge/Tests-112%20passing-brightgreen) + +🇩🇪 [Deutsche Version → README.de.md](README.de.md) + +**Website:** [bavarianrankengine.com](https://bavarianrankengine.com)  ·  [How To](https://bavarianrankengine.com/howto.html)  ·  [FAQ](https://bavarianrankengine.com/faq.html)  ·  [Changelog](https://bavarianrankengine.com/changelog.html) + +--- + +Bavarian Rank Engine 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. + +BRE takes a different approach: + +- **Direct API access.** You store your own key from OpenAI, Anthropic, Google, or xAI. BRE 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.** BRE 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 + +``` +bavarian-rank-engine/ +├── bavarian-rank-engine.php # Plugin header, constants (BRE_VERSION, BRE_DIR, BRE_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 .bre-geo on frontend +│ ├── 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 +│ │ ├── 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 +│ │ ├── 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 +│ │ └── 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) | BRE 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 **Bavarian Rank → 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, BRE shows an admin notice — BRE 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; BRE 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** (`bre_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}bre_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 (`bre_cleanup_crawler_log`). The dashboard shows a 30-day summary per bot. + +--- + +### 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 (`bre_link_analysis`). + +--- + +## Data Storage + +### WordPress Options (wp_options) + +| Option Key | Content | +|---|---| +| `bre_settings` | Active provider, API keys (obfuscated), model selection, token costs, `ai_enabled` flag | +| `bre_meta_settings` | Meta generator: auto mode, post types, token mode, prompt | +| `bre_schema_settings` | Schema.org: enabled types, organization sameAs URLs | +| `bre_geo_settings` | GEO block: mode, position, labels, CSS, prompt, color scheme | +| `bre_robots_settings` | robots.txt: blocked bots | +| `bre_llms_settings` | llms.txt: title, description, featured links, footer, page count | +| `bre_usage_stats` | Accumulated token usage: `tokens_in`, `tokens_out`, `count` | +| `bre_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) | + +### Custom Database Table + +| Table | Purpose | +|---|---| +| `{prefix}bre_crawler_log` | AI bot visits (bot_name, ip_hash, url, visited_at) | + +### Transients + +| Transient | TTL | Purpose | +|---|---|---| +| `bre_llms_cache_{n}` | 1 hour | Cached llms.txt content per page | +| `bre_link_analysis` | 1 hour | Dashboard link analysis result | +| `bre_bulk_running` | 15 minutes | Mutex lock for bulk generator | +| `bre_meta_stats` | 5 minutes | Dashboard meta coverage query result | +| `bre_crawler_summary` | 5 minutes | Dashboard crawler summary (last 30 days) | + +### Uninstall cleanup + +`uninstall.php` removes on plugin deletion: +- Option `bre_settings` +- Post meta `_bre_meta_description` for all posts + +> Note: The remaining option keys and the `bre_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:" +``` + +`BavarianRankEngine\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( 'BRE_OPENAI_KEY', 'sk-...' ); +define( 'BRE_ANTHROPIC_KEY', 'sk-ant-...' ); +define( 'BRE_GEMINI_KEY', 'AI...' ); +define( 'BRE_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( 'bre_admin', 'nonce' ); // CSRF +if ( ! current_user_can( 'manage_options' ) ) { // Authorization + wp_send_json_error( 'Unauthorized', 403 ); +} +``` + +The nonce `bre_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 + +BRE 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 BavarianRankEngine\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 + +### `bre_prompt` (Filter) + +Allows modifying the final prompt immediately before the API call. + +```php +add_filter( 'bre_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 ); +``` + +### `bre_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( 'bre_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 BRE_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 | +|---|---|---| +| `bre_regen_meta` | `MetaEditorBox::ajax_regen` | Regenerate meta description for a single post | +| `bre_test_connection` | `ProviderPage::ajax_test_connection` | Test API key and connection | +| `bre_get_default_prompt` | `ProviderPage::ajax_get_default_prompt` | Reset to default prompt | +| `bre_link_analysis` | `LinkAnalysis::ajax_analyse` | Run link analysis for the dashboard | +| `bre_link_suggestions` | `LinkSuggest::ajax_suggest` | Return top-10 internal link suggestions for current post | +| `bre_geo_generate` | `GeoEditorBox::ajax_generate` | Generate GEO block for a single post | +| `bre_geo_clear` | `GeoEditorBox::ajax_clear` | Clear GEO block data for a single post | +| `bre_llms_clear_cache` | `TxtPage::ajax_clear_cache` | Clear llms.txt transient cache | +| `bre_dismiss_llms_notice` | `LlmsTxt::ajax_dismiss_notice` | Dismiss Rank Math conflict admin notice | +| `bre_dismiss_welcome` | `AdminMenu::ajax_dismiss_welcome` | Dismiss the welcome notice per user | +| `bre_bulk_generate` | `MetaGenerator::ajaxBulkGenerate` | Process next batch in bulk generator | +| `bre_bulk_stats` | `MetaGenerator::ajaxBulkStats` | Retrieve progress and stats of running bulk | +| `bre_bulk_release` | `MetaGenerator::ajaxBulkRelease` | Manually release bulk mutex lock | +| `bre_bulk_status` | `MetaGenerator::ajaxBulkStatus` | Check bulk lock status | + +--- + +## Installation + +**Via GitHub Release (recommended):** +1. Download `bavarian-rank-engine.zip` from the [latest release](https://github.com/noschmarrn/bavarianrankengine/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/bavarianrankengine.git bavarian-rank-engine +wp plugin activate bavarian-rank-engine +``` + +**After activation:** +1. Go to *Bavarian Rank → 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 | `BavarianRankEngine\` | +| 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 `bavarian-rank-engine` | +| Tests | PHPUnit (102 tests, 216 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) diff --git a/brezngeo/LICENSE b/brezngeo/LICENSE new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/brezngeo/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/brezngeo/assets/admin.css b/brezngeo/assets/admin.css new file mode 100644 index 0000000..652935e --- /dev/null +++ b/brezngeo/assets/admin.css @@ -0,0 +1,137 @@ +.brezngeo-settings h1 { + padding-left: 4px; + margin-bottom: 16px; +} +.brezngeo-settings h2 { + border-bottom: 1px solid #ddd; + padding-bottom: 5px; + margin-top: 30px; +} +.brezngeo-dashboard-grid .postbox { + margin: 0; +} +.brezngeo-footer { + margin-top: 24px; + padding-top: 12px; + border-top: 1px solid #eee; + color: #999; + font-size: 12px; +} +.brezngeo-provider-row { display: none; } +.brezngeo-provider-row.active { display: table-row; } +.brezngeo-test-result { margin-left: 10px; font-weight: bold; } +.brezngeo-test-result.success { color: #46b450; } +.brezngeo-test-result.error { color: #dc3232; } + + +/* --- Welcome notice --- */ +.brezngeo-welcome-notice { + background: linear-gradient(135deg, #fff 0%, #f0f6ff 100%); + border-left: 4px solid #2271b1; + border-radius: 0 4px 4px 0; + padding: 16px 40px 16px 20px; + margin: 16px 0 0; + position: relative; + box-shadow: 0 1px 4px rgba(0,0,0,.06); +} +.brezngeo-welcome-notice a { font-weight: 600; } +.brezngeo-dismiss { + position: absolute; + top: 10px; + right: 12px; + background: none; + border: none; + cursor: pointer; + font-size: 20px; + color: #999; + line-height: 1; + padding: 2px 6px; +} +.brezngeo-dismiss:hover { color: #333; } + +/* --- Progress bars --- */ +.brezngeo-progress-bar { + height: 8px; + background: #f0f0f1; + border-radius: 4px; + overflow: hidden; +} +.brezngeo-progress-fill { + height: 100%; + border-radius: 4px; + transition: width .3s ease; +} +.brezngeo-progress-fill.brezngeo-ok { background: #46b450; } +.brezngeo-progress-fill.brezngeo-warn { background: #ffb900; } +.brezngeo-progress-fill.brezngeo-bad { background: #dc3232; } + +/* --- Quick links --- */ +.brezngeo-quick-links-list { + list-style: none; + margin: 0; + padding: 0; +} +.brezngeo-quick-links-list li { + border-bottom: 1px solid #f0f0f1; +} +.brezngeo-quick-links-list li:last-child { border-bottom: none; } +.brezngeo-quick-links-list a { + display: block; + padding: 9px 6px; + text-decoration: none; + color: #2271b1; + transition: padding-left .12s; +} +.brezngeo-quick-links-list a:hover { + color: #135e96; + padding-left: 12px; +} + +/* --- Crawler dot --- */ +.brezngeo-bot-dot { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + background: #46b450; + margin-right: 6px; + vertical-align: middle; +} + +/* --- Meta coverage --- */ +.brezngeo-coverage-row { margin-bottom: 14px; } +.brezngeo-coverage-label { + display: flex; + justify-content: space-between; + margin-bottom: 4px; + font-size: 13px; +} +.brezngeo-coverage-stat { color: #666; } + +/* --- Status widget --- */ +.brezngeo-stat-label { + padding: 5px 8px 5px 0; + color: #666; + font-size: 12px; + width: 55%; + vertical-align: top; +} +.brezngeo-stat-value { + padding: 5px 0; + font-weight: 600; + font-size: 13px; +} + +/* --- AI enable toggle --- */ +.brezngeo-ai-toggle-wrap { + background: #fff8e5; + border-left: 4px solid #ffb900; + border-radius: 0 4px 4px 0; + padding: 14px 16px; + margin-bottom: 24px; +} +.brezngeo-ai-cost-notice { + margin: 8px 0 0; + color: #856404; + font-size: 13px; +} diff --git a/brezngeo/assets/admin.js b/brezngeo/assets/admin.js new file mode 100644 index 0000000..7d6787b --- /dev/null +++ b/brezngeo/assets/admin.js @@ -0,0 +1,126 @@ +/* global brezngeoAdmin, brezngeoL10n */ +jQuery( function ( $ ) { + function updateProviderRows() { + var active = $( '#brezngeo-provider' ).val(); + $( '.brezngeo-provider-row' ).removeClass( 'active' ); + $( '.brezngeo-provider-row[data-provider="' + active + '"]' ).addClass( 'active' ); + } + updateProviderRows(); + $( '#brezngeo-provider' ).on( 'change', updateProviderRows ); + + $( document ).on( 'click', '.brezngeo-test-btn', function () { + var btn = $( this ); + var providerId = btn.data( 'provider' ); + var resultEl = $( '#test-result-' + providerId ); + + resultEl.removeClass( 'success error' ).text( brezngeoAdmin.testing ); + btn.prop( 'disabled', true ); + + $.post( brezngeoAdmin.ajaxUrl, { + action: 'brezngeo_test_connection', + nonce: brezngeoAdmin.nonce, + provider: providerId, + } ).done( function ( res ) { + if ( res.success ) { + resultEl.addClass( 'success' ).text( '\u2713 ' + res.data ); + } else { + resultEl.addClass( 'error' ).text( '\u2717 ' + res.data ); + } + } ).fail( function () { + resultEl.addClass( 'error' ).text( '\u2717 ' + brezngeoAdmin.networkError ); + } ).always( function () { + btn.prop( 'disabled', false ); + } ); + } ); + + $( '#brezngeo-reset-prompt' ).on( 'click', function () { + if ( ! confirm( brezngeoAdmin.resetConfirm ) ) return; + $.post( brezngeoAdmin.ajaxUrl, { + action: 'brezngeo_get_default_prompt', + nonce: brezngeoAdmin.nonce, + } ).done( function ( res ) { + if ( res.success ) { + $( 'textarea[name*="prompt"]' ).val( res.data ); + } + } ); + } ); + + $( '#brezngeo-dismiss-welcome' ).on( 'click', function () { + $( '#brezngeo-welcome-notice' ).slideUp( 200 ); + $.post( brezngeoAdmin.ajaxUrl, { + action: 'brezngeo_dismiss_welcome', + nonce: brezngeoAdmin.nonce, + } ); + } ); + + function updateAiFields() { + if ( $( '#brezngeo-ai-enabled' ).is( ':checked' ) ) { + $( '#brezngeo-ai-fields' ).show(); + } else { + $( '#brezngeo-ai-fields' ).hide(); + } + } + if ( $( '#brezngeo-ai-enabled' ).length ) { + updateAiFields(); + $( '#brezngeo-ai-enabled' ).on( 'change', updateAiFields ); + } + + // llms.txt cache clear button + $( '#brezngeo-llms-clear-cache' ).on( 'click', function () { + $.post( brezngeoAdmin.ajaxUrl, { + action: 'brezngeo_llms_clear_cache', + nonce: brezngeoAdmin.nonce, + } ).done( function ( res ) { + $( '#brezngeo-cache-result' ).text( res.success ? brezngeoAdmin.cacheCleared : brezngeoAdmin.error ); + setTimeout( function () { $( '#brezngeo-cache-result' ).text( '' ); }, 3000 ); + } ); + } ); + + // Link analysis dashboard widget + if ( typeof brezngeoL10n !== 'undefined' && $( '#brezngeo-link-analysis-content' ).length ) { + $.post( brezngeoAdmin.ajaxUrl, { + action: 'brezngeo_link_analysis', + nonce: brezngeoAdmin.nonce, + } ).done( function ( res ) { + if ( ! res.success ) { + $( '#brezngeo-link-analysis-content' ).text( brezngeoL10n.analysisError ); + return; + } + var d = res.data, h = ''; + h += '

' + brezngeoL10n.noLinksHeading + ' (' + d.no_internal_links.length + ')

'; + if ( d.no_internal_links.length ) { + h += '
    '; + $.each( d.no_internal_links.slice( 0, 10 ), function ( i, p ) { + h += '
  • ' + $( '' ).text( p.title ).html() + '
  • '; + } ); + if ( d.no_internal_links.length > 10 ) h += '
  • \u2026
  • '; + h += '
'; + } else { + h += '

' + brezngeoL10n.allLinked + '

'; + } + h += '

' + brezngeoL10n.manyExternalPre + d.threshold + ')

'; + if ( d.too_many_external.length ) { + h += '
    '; + $.each( d.too_many_external.slice( 0, 5 ), function ( i, p ) { + h += '
  • ' + $( '' ).text( p.title ).html() + ' (' + p.count + ')
  • '; + } ); + h += '
'; + } else { + h += '

' + brezngeoL10n.noExternalIssues + '

'; + } + h += '

' + brezngeoL10n.pillarHeading + '

'; + if ( d.pillar_pages.length ) { + h += ''; + } else { + h += '

' + brezngeoL10n.noData + '

'; + } + $( '#brezngeo-link-analysis-content' ).html( h ); + } ).fail( function () { + $( '#brezngeo-link-analysis-content' ).text( brezngeoL10n.connectionError ); + } ); + } +} ); diff --git a/brezngeo/assets/bulk.js b/brezngeo/assets/bulk.js new file mode 100644 index 0000000..35a8a7c --- /dev/null +++ b/brezngeo/assets/bulk.js @@ -0,0 +1,221 @@ +/* global brezngeoBulk */ +jQuery( function ( $ ) { + var running = false; + var stopFlag = false; + var processed = 0; + var total = 0; + var failedItems = []; + + if ( brezngeoBulk.isLocked ) { + showLockWarning( brezngeoBulk.lockAge ); + } + + loadStats(); + + function showLockWarning( age ) { + var msg = brezngeoBulk.i18n.lockWarning + ( age ? ' (' + brezngeoBulk.i18n.since + ' ' + age + 's)' : '' ) + '.'; + $( '#brezngeo-lock-warning' ).text( msg ).show(); + $( '#brezngeo-bulk-start' ).prop( 'disabled', true ); + } + + function hideLockWarning() { + $( '#brezngeo-lock-warning' ).hide(); + $( '#brezngeo-bulk-start' ).prop( 'disabled', false ); + } + + function loadStats() { + $.post( brezngeoBulk.ajaxUrl, { action: 'brezngeo_bulk_stats', nonce: brezngeoBulk.nonce } ) + .done( function ( res ) { + if ( ! res.success ) return; + var html = '' + brezngeoBulk.i18n.postsWithoutMeta + '
    '; + var t = 0; + $.each( res.data, function ( pt, count ) { + html += '
  • ' + $( '' ).text( pt ).html() + ': ' + parseInt( count, 10 ) + '
  • '; + t += parseInt( count, 10 ); + } ); + html += '
' + brezngeoBulk.i18n.total + ' ' + t + ''; + total = t; + $( '#brezngeo-bulk-stats' ).html( html ); + updateCostEstimate(); + } ); + } + + $( '#brezngeo-bulk-limit, #brezngeo-bulk-model, #brezngeo-bulk-provider' ).on( 'change', updateCostEstimate ); + + function updateCostEstimate() { + var limit = parseInt( $( '#brezngeo-bulk-limit' ).val(), 10 ) || 20; + var inputTokens = limit * 800; + var outputTokens = limit * 50; + var costHtml = '~' + inputTokens + ' ' + brezngeoBulk.i18n.inputTokens + ' + ' + outputTokens + ' ' + brezngeoBulk.i18n.outputTokens; + + var costData = brezngeoBulk.costs || {}; + var provider = $( '#brezngeo-bulk-provider' ).val(); + var model = $( '#brezngeo-bulk-model' ).val(); + + if ( costData[ provider ] && costData[ provider ][ model ] ) { + var c = costData[ provider ][ model ]; + var inCost = ( inputTokens / 1000000 ) * parseFloat( c.input || 0 ); + var outCost= ( outputTokens / 1000000 ) * parseFloat( c.output || 0 ); + var total = inCost + outCost; + if ( total > 0 ) { + costHtml += ' \u2248 $' + total.toFixed( 4 ); + } + } + $( '#brezngeo-cost-estimate' ).text( costHtml ); + } + + $( '#brezngeo-bulk-start' ).on( 'click', function () { + if ( running ) return; + $.post( brezngeoBulk.ajaxUrl, { action: 'brezngeo_bulk_status', nonce: brezngeoBulk.nonce } ) + .done( function ( res ) { + if ( res.success && res.data.locked ) { + showLockWarning( res.data.lock_age ); + return; + } + startRun(); + } ); + } ); + + function startRun() { + running = true; + stopFlag = false; + processed = 0; + failedItems = []; + + $( '#brezngeo-bulk-start' ).prop( 'disabled', true ); + $( '#brezngeo-bulk-stop' ).show(); + $( '#brezngeo-progress-wrap' ).show(); + $( '#brezngeo-bulk-log' ).show().html( '' ); + $( '#brezngeo-failed-summary' ).hide().html( '' ); + hideLockWarning(); + + var limit = parseInt( $( '#brezngeo-bulk-limit' ).val(), 10 ) || 20; + var provider = $( '#brezngeo-bulk-provider' ).val(); + var model = $( '#brezngeo-bulk-model' ).val(); + + log( brezngeoBulk.i18n.logStart.replace( '{limit}', limit ).replace( '{provider}', provider ) ); + runBatch( 'post', limit, provider, model, true ); + } + + $( '#brezngeo-bulk-stop' ).on( 'click', function () { + stopFlag = true; + log( '\u26A0 ' + brezngeoBulk.i18n.stopRequested, 'warn' ); + releaseLock(); + } ); + + function releaseLock() { + $.post( brezngeoBulk.ajaxUrl, { action: 'brezngeo_bulk_release', nonce: brezngeoBulk.nonce } ); + } + + function runBatch( postType, remaining, provider, model, isFirst ) { + if ( stopFlag || remaining <= 0 ) { + finish(); + return; + } + + var batchSize = Math.min( 20, remaining ); + var isLast = ( remaining - batchSize ) <= 0; + + log( brezngeoBulk.i18n.logProcess.replace( '{count}', batchSize ).replace( '{remaining}', remaining ) ); + + $.post( brezngeoBulk.ajaxUrl, { + action: 'brezngeo_bulk_generate', + nonce: brezngeoBulk.nonce, + post_type: postType, + batch_size: batchSize, + provider: provider, + model: model, + is_first: isFirst ? 1 : 0, + is_last: isLast ? 1 : 0, + } ).done( function ( res ) { + if ( ! res.success ) { + if ( res.data && res.data.locked ) { + showLockWarning( res.data.lock_age ); + finish(); + return; + } + log( '\u2717 Fehler: ' + $( '' ).text( ( res.data && res.data.message ) || brezngeoBulk.i18n.unknownError ).html(), 'error' ); + finish(); + return; + } + + $.each( res.data.results, function ( i, item ) { + if ( item.success ) { + var note = item.attempts > 1 ? ' (' + brezngeoBulk.i18n.attempt + ' ' + item.attempts + ')' : ''; + log( + '\u2713 [' + item.id + '] ' + + $( '' ).text( item.title ).html() + note + + '
' + + $( '' ).text( item.description ).html() + + '' + ); + } else { + failedItems.push( item ); + log( + '\u2717 [' + item.id + '] ' + + $( '' ).text( item.title ).html() + + ' \u2014 ' + $( '' ).text( item.error ).html(), + 'error' + ); + } + processed++; + } ); + + updateProgress( processed, total ); + + var newRemaining = remaining - batchSize; + if ( res.data.remaining > 0 && ! stopFlag && newRemaining > 0 ) { + setTimeout( function () { + runBatch( postType, newRemaining, provider, model, false ); + }, brezngeoBulk.rateDelay ); + } else { + if ( isLast || res.data.remaining === 0 ) releaseLock(); + finish(); + } + } ).fail( function () { + log( '\u2717 ' + brezngeoBulk.i18n.networkError, 'error' ); + releaseLock(); + finish(); + } ); + } + + function updateProgress( done, t ) { + var pct = t > 0 ? Math.round( ( done / t ) * 100 ) : 100; + $( '#brezngeo-progress-bar' ).css( 'width', pct + '%' ); + $( '#brezngeo-progress-text' ).text( done + ' / ' + t + ' ' + brezngeoBulk.i18n.processed ); + } + + /** + * Append a line to the log console. + * @param {string} msg Pre-escaped HTML string. User data MUST be escaped via + * $('').text(val).html() before passing here. + * @param {string} type 'error' | 'warn' | undefined + */ + function log( msg, type ) { + var color = type === 'error' ? '#f48771' : type === 'warn' ? '#dcdcaa' : '#9cdcfe'; + $( '#brezngeo-bulk-log' ).append( + '
' + msg + '
' + ); + var el = document.getElementById( 'brezngeo-bulk-log' ); + el.scrollTop = el.scrollHeight; + } + + function finish() { + running = false; + $( '#brezngeo-bulk-start' ).prop( 'disabled', false ); + $( '#brezngeo-bulk-stop' ).hide(); + log( brezngeoBulk.i18n.done ); + + if ( failedItems.length > 0 ) { + var html = '\u26A0 ' + failedItems.length + ' ' + brezngeoBulk.i18n.postsFailed + '
    '; + $.each( failedItems, function ( i, item ) { + html += '
  • [' + item.id + '] ' + + $( '' ).text( item.title ).html() + + ': ' + $( '' ).text( item.error ).html() + '
  • '; + } ); + html += '
'; + $( '#brezngeo-failed-summary' ).html( html ).show(); + } + loadStats(); + } +} ); diff --git a/brezngeo/assets/editor-meta.js b/brezngeo/assets/editor-meta.js new file mode 100644 index 0000000..d2c3397 --- /dev/null +++ b/brezngeo/assets/editor-meta.js @@ -0,0 +1,32 @@ +/* global jQuery, ajaxurl */ +jQuery( function ( $ ) { + var $textarea = $( '#brezngeo-meta-description' ); + var $count = $( '#brezngeo-meta-count' ); + var $btn = $( '#brezngeo-regen-meta' ); + + if ( ! $textarea.length ) return; + + $textarea.on( 'input', function () { + $count.text( $( this ).val().length + ' / 160' ); + } ); + + if ( ! $btn.length ) return; + + $btn.on( 'click', function () { + $btn.prop( 'disabled', true ).text( '…' ); + $.post( ajaxurl, { + action: 'brezngeo_regen_meta', + nonce: $btn.data( 'nonce' ), + post_id: $btn.data( 'post-id' ), + } ).done( function ( res ) { + if ( res.success ) { + $textarea.val( res.data.description ); + $count.text( res.data.description.length + ' / 160' ); + } else { + alert( 'Fehler: ' + ( res.data || 'Unbekannt' ) ); + } + } ).always( function () { + $btn.prop( 'disabled', false ).text( 'Mit KI neu generieren' ); + } ); + } ); +} ); diff --git a/brezngeo/assets/geo-editor.js b/brezngeo/assets/geo-editor.js new file mode 100644 index 0000000..83c418b --- /dev/null +++ b/brezngeo/assets/geo-editor.js @@ -0,0 +1,84 @@ +/* global jQuery, ajaxurl */ +jQuery( function ( $ ) { + var $box = $( '#brezngeo-geo-box' ); + if ( ! $box.length ) return; + + var postId = $box.data( 'post-id' ); + var nonce = $box.data( 'nonce' ); + var $generate = $( '#brezngeo-geo-generate' ); + var $clear = $( '#brezngeo-geo-clear' ); + var $status = $( '#brezngeo-geo-status' ); + var $summary = $( '#brezngeo-geo-summary' ); + var $bullets = $( '#brezngeo-geo-bullets' ); + var $faq = $( '#brezngeo-geo-faq' ); + var $lock = $( '#brezngeo-geo-lock' ); + + function setStatus( msg, isError ) { + $status.text( msg ).css( 'color', isError ? '#dc3232' : '#46b450' ); + if ( msg ) { + setTimeout( function () { $status.text( '' ); }, 4000 ); + } + } + + function populateFields( data ) { + $summary.val( data.summary || '' ); + $bullets.val( ( data.bullets || [] ).join( '\n' ) ); + var faqLines = ( data.faq || [] ).map( function ( item ) { + return item.q + ' | ' + item.a; + } ); + $faq.val( faqLines.join( '\n' ) ); + // AI-generated content resets the lock + $lock.prop( 'checked', false ); + } + + // Track manual edits → auto-set lock to protect from overwrite + $summary.add( $bullets ).add( $faq ).on( 'input', function () { + $lock.prop( 'checked', true ); + } ); + + if ( $generate.length ) { + $generate.on( 'click', function () { + $generate.prop( 'disabled', true ).text( '…' ); + setStatus( '' ); + $.post( ajaxurl, { + action: 'brezngeo_geo_generate', + nonce: nonce, + post_id: postId, + } ).done( function ( res ) { + if ( res.success ) { + populateFields( res.data ); + setStatus( 'Generated ✓', false ); + $generate.text( 'Regenerate' ); + } else { + setStatus( res.data || 'Error', true ); + } + } ).fail( function () { + setStatus( 'Connection error', true ); + } ).always( function () { + $generate.prop( 'disabled', false ); + } ); + } ); + } + + if ( $clear.length ) { + $clear.on( 'click', function () { + if ( ! window.confirm( 'Really clear GEO fields?' ) ) return; + $clear.prop( 'disabled', true ); + $.post( ajaxurl, { + action: 'brezngeo_geo_clear', + nonce: nonce, + post_id: postId, + } ).done( function ( res ) { + if ( res.success ) { + $summary.val( '' ); + $bullets.val( '' ); + $faq.val( '' ); + $lock.prop( 'checked', false ); + setStatus( 'Cleared', false ); + } + } ).always( function () { + $clear.prop( 'disabled', false ); + } ); + } ); + } +} ); diff --git a/brezngeo/assets/geo-frontend.css b/brezngeo/assets/geo-frontend.css new file mode 100644 index 0000000..77c1104 --- /dev/null +++ b/brezngeo/assets/geo-frontend.css @@ -0,0 +1,160 @@ +/* BreznGEO — GEO Block (scoped to .brezngeo-geo) */ + +/* ── Base layout + Light theme (default) ──────────────── */ +.brezngeo-geo, +.brezngeo-geo[data-brezngeo-theme="light"] { + --brezngeo-border: #e0e0e0; + --brezngeo-bg: #fafafa; + --brezngeo-sec-border:#e0e0e0; + --brezngeo-label: #666; + --brezngeo-faq-ans: #444; + --brezngeo-accent: #0073aa; + + margin: 1.5em 0; + border: 1px solid var(--brezngeo-border); + border-radius: 6px; + background: var(--brezngeo-bg); + padding: 0; +} + +/* ── Dark theme ───────────────────────────────────────── */ +.brezngeo-geo[data-brezngeo-theme="dark"] { + --brezngeo-border: #3d3d3d; + --brezngeo-bg: #1e1e1e; + --brezngeo-sec-border:#3d3d3d; + --brezngeo-label: #999; + --brezngeo-faq-ans: #bbb; + --brezngeo-accent: #4ea8d8; + + border: 1px solid var(--brezngeo-border); + border-radius: 6px; + background: var(--brezngeo-bg); + padding: 0; +} + +/* ── Minimal theme ────────────────────────────────────── */ +.brezngeo-geo[data-brezngeo-theme="minimal"] { + --brezngeo-sec-border:#efefef; + --brezngeo-label: #999; + --brezngeo-faq-ans: #666; + + margin: 1.5em 0; + border: none; + border-left: 2px solid #d0d0d0; + border-radius: 0; + background: transparent; + padding: 0 0 0 0.75em; +} + +/* ── Bavarian theme ───────────────────────────────────── */ +.brezngeo-geo[data-brezngeo-theme="bavarian"] { + --brezngeo-border: #0052a0; + --brezngeo-bg: #f0f5fc; + --brezngeo-sec-border:#c5d8f5; + --brezngeo-label: #003d82; + --brezngeo-faq-ans: #2a4a7f; + --brezngeo-accent: #0066b3; + + border: 1px solid var(--brezngeo-border); + border-radius: 6px; + background: var(--brezngeo-bg); + padding: 0; +} + +/* ── Summary bar ──────────────────────────────────────── */ +.brezngeo-geo summary { + cursor: pointer; + padding: 0.75em 1em; + font-weight: 600; + list-style: none; + display: flex; + align-items: center; + border-left: 3px solid var(--brezngeo-accent); + border-radius: 5px 5px 0 0; +} + +.brezngeo-geo summary::-webkit-details-marker { display: none; } + +.brezngeo-geo summary::before { + content: '▶'; + display: inline-block; + margin-right: 0.5em; + font-size: 0.7em; + transition: transform 0.2s; + color: var(--brezngeo-accent); +} + +.brezngeo-geo[open] summary::before { transform: rotate(90deg); } + +.brezngeo-geo__title { flex: 1; } + +/* ── Minimal summary override ─────────────────────────── */ +.brezngeo-geo[data-brezngeo-theme="minimal"] summary { + padding: 0.5em 0; + font-weight: 500; + border-left: none; +} + +.brezngeo-geo[data-brezngeo-theme="minimal"] summary::before { + color: #aaa; +} + +/* ── Bavarian summary: Rauten (diamond) pattern ───────── */ +.brezngeo-geo[data-brezngeo-theme="bavarian"] summary { + background-color: #0066b3; + background-image: + linear-gradient(45deg, rgba(255,255,255,0.18) 25%, transparent 25%), + linear-gradient(-45deg, rgba(255,255,255,0.18) 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, rgba(255,255,255,0.18) 75%), + linear-gradient(-45deg, transparent 75%, rgba(255,255,255,0.18) 75%); + background-size: 10px 10px; + background-position: 0 0, 0 5px, 5px -5px, -5px 0; + color: #fff; + border-left-color: #003d82; + border-radius: 5px 5px 0 0; +} + +.brezngeo-geo[data-brezngeo-theme="bavarian"] summary .brezngeo-geo__title { + color: #fff; +} + +.brezngeo-geo[data-brezngeo-theme="bavarian"] summary::before { + color: rgba(255,255,255,0.85); +} + +/* ── Content sections ─────────────────────────────────── */ +.brezngeo-geo__section { + padding: 0.75em 1em; + border-top: 1px solid var(--brezngeo-sec-border); +} + +.brezngeo-geo[data-brezngeo-theme="minimal"] .brezngeo-geo__section { + padding: 0.5em 0; +} + +.brezngeo-geo__section h3 { + font-size: 0.8em; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--brezngeo-label); + margin: 0 0 0.5em; +} + +.brezngeo-geo__bullets ul { + margin: 0; + padding-left: 1.25em; +} + +.brezngeo-geo__bullets li { margin-bottom: 0.25em; } + +.brezngeo-geo__faq dl { margin: 0; } + +.brezngeo-geo__faq dt { + font-weight: 600; + margin-top: 0.5em; +} + +.brezngeo-geo__faq dd { + margin-left: 0; + color: var(--brezngeo-faq-ans); +} diff --git a/brezngeo/assets/link-suggest.js b/brezngeo/assets/link-suggest.js new file mode 100644 index 0000000..aa2797f --- /dev/null +++ b/brezngeo/assets/link-suggest.js @@ -0,0 +1,300 @@ +/* global jQuery, wp, bavrankLinkSuggest, tinyMCE */ +( function ( $ ) { + 'use strict'; + + if ( typeof bavrankLinkSuggest === 'undefined' ) { return; } + + var cfg = bavrankLinkSuggest; + var i18n = cfg.i18n; + var suggestions = []; + var isRunning = false; + + /* ── Helpers ─────────────────────────────────────────── */ + + function getContent() { + if ( window.wp && wp.data && wp.data.select( 'core/editor' ) ) { + try { + return wp.data.select( 'core/editor' ).getEditedPostContent(); + } catch ( e ) { /* fall through */ } + } + if ( typeof tinyMCE !== 'undefined' && tinyMCE.activeEditor && ! tinyMCE.activeEditor.isHidden() ) { + return tinyMCE.activeEditor.getContent(); + } + return $( '#content' ).val() || ''; + } + + /* ── Core: request suggestions ───────────────────────── */ + + function triggerAnalysis() { + if ( isRunning ) { return; } + var content = getContent(); + if ( ! content ) { return; } + + isRunning = true; + $( '#brezngeo-ls-status' ).text( i18n.loading ); + $( '#brezngeo-ls-results' ).hide(); + $( '#brezngeo-ls-applied' ).hide(); + + $.post( cfg.ajaxUrl, { + action: 'brezngeo_link_suggestions', + nonce: cfg.nonce, + post_id: cfg.postId, + post_content: content, + } ) + .done( function ( res ) { + if ( res && res.success ) { + suggestions = res.data || []; + renderSuggestions(); + } else { + $( '#brezngeo-ls-status' ).text( i18n.networkError ); + } + } ) + .fail( function () { + $( '#brezngeo-ls-status' ).text( i18n.networkError ); + } ) + .always( function () { + isRunning = false; + } ); + } + + /* ── Render suggestion list ──────────────────────────── */ + + function renderSuggestions() { + var $list = $( '#brezngeo-ls-list' ).empty(); + var $results = $( '#brezngeo-ls-results' ); + var $actions = $( '#brezngeo-ls-actions' ); + var $status = $( '#brezngeo-ls-status' ); + + if ( ! suggestions.length ) { + $status.text( i18n.noResults ); + $results.hide(); + return; + } + + $status.text( '' ); + + suggestions.forEach( function ( s, idx ) { + var $row = $( '
' ); + var $cb = $( '' ).data( 'idx', idx ); + var $info = $( '
' ); + var badge = s.boosted ? ' ' : ''; + $info.html( + '' + esc( '\u201c' + s.phrase + '\u201d' ) + '' + badge + + '
\u2192 ' + esc( s.post_title ) + '' + ); + var $open = $( '[↗]' ); + $row.append( $cb, $info, $open ); + $list.append( $row ); + } ); + + $results.show(); + $actions.css( 'display', 'flex' ); + updateApplyButton(); + } + + function esc( str ) { + return $( '
' ).text( str ).html(); + } + + function updateApplyButton() { + var count = $( '.brezngeo-ls-cb:checked' ).length; + var label = i18n.applyBtn.replace( '%d', count ); + $( '#brezngeo-ls-apply' ).text( label ).prop( 'disabled', count === 0 ); + } + + /* ── Apply selected ──────────────────────────────────── */ + + function applySelected() { + var selected = []; + $( '.brezngeo-ls-cb:checked' ).each( function () { + var idx = $( this ).data( 'idx' ); + if ( suggestions[ idx ] ) { + selected.push( suggestions[ idx ] ); + } + } ); + if ( ! selected.length ) { return; } + + // Build preview + var lines = selected.map( function ( s ) { + return '\u201c' + s.phrase + '\u201d \u2192 ' + s.post_title; + } ).join( '\n' ); + + // eslint-disable-next-line no-alert + if ( ! window.confirm( i18n.preview + ':\n\n' + lines + '\n\n' + i18n.confirm + '?' ) ) { + return; + } + + var content = getContent(); + var applied = 0; + selected.forEach( function ( s ) { + var result = insertLink( content, s.phrase, s.url ); + if ( result !== content ) { + content = result; + applied++; + } + } ); + + if ( applied ) { + setContent( content ); + $( '#brezngeo-ls-results' ).hide(); + $( '#brezngeo-ls-applied' ).text( i18n.applied.replace( '%d', applied ) ).show(); + suggestions = []; + } + } + + /** + * Replace first occurrence of phrase (outside ) with a link. + */ + function insertLink( html, phrase, url ) { + var escaped = phrase.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' ); + var re = new RegExp( '(?])(' + escaped + ')(?![^<]*)', 'i' ); + var replaced = false; + return html.replace( re, function ( match ) { + if ( replaced ) { return match; } + replaced = true; + return '' + match + ''; + } ); + } + + function setContent( html ) { + // Gutenberg + if ( window.wp && wp.data && wp.data.dispatch ) { + try { + var blocks = wp.blocks.parse( html ); + wp.data.dispatch( 'core/block-editor' ).resetBlocks( blocks ); + return; + } catch ( e ) { /* fall through to classic */ } + } + // Classic / TinyMCE + if ( typeof tinyMCE !== 'undefined' && tinyMCE.activeEditor && ! tinyMCE.activeEditor.isHidden() ) { + tinyMCE.activeEditor.setContent( html ); + return; + } + $( '#content' ).val( html ); + } + + /* ── Event bindings ──────────────────────────────────── */ + + $( document ).on( 'click', '#brezngeo-ls-analyse', triggerAnalysis ); + $( document ).on( 'click', '#brezngeo-ls-apply', applySelected ); + $( document ).on( 'click', '#brezngeo-ls-select-all', function () { $( '.brezngeo-ls-cb' ).prop( 'checked', true ); updateApplyButton(); } ); + $( document ).on( 'click', '#brezngeo-ls-select-none', function () { $( '.brezngeo-ls-cb' ).prop( 'checked', false ); updateApplyButton(); } ); + $( document ).on( 'change', '.brezngeo-ls-cb', updateApplyButton ); + + /* ── Trigger mode ────────────────────────────────────── */ + + if ( cfg.triggerMode === 'interval' && cfg.intervalMs > 0 ) { + setInterval( triggerAnalysis, cfg.intervalMs ); + } + + if ( cfg.triggerMode === 'save' ) { + // Gutenberg + if ( window.wp && wp.data ) { + var wasSaving = false; + wp.data.subscribe( function () { + var isSaving = wp.data.select( 'core/editor' ) && + wp.data.select( 'core/editor' ).isSavingPost(); + if ( ! wasSaving && isSaving ) { + triggerAnalysis(); + } + wasSaving = isSaving; + } ); + } + // Classic + $( document ).on( 'click', '#publish, #save-post', function () { + setTimeout( triggerAnalysis, 500 ); + } ); + } + + /* ── Settings page: post search for exclude/boost ────── */ + + function initPostSearch( $input, $results, onSelect ) { + var timer; + $input.on( 'input', function () { + clearTimeout( timer ); + var q = $input.val().trim(); + if ( q.length < 2 ) { $results.hide(); return; } + timer = setTimeout( function () { + $.ajax( { + url: cfg.restUrl, + data: { search: q, type: 'post', subtype: 'any', per_page: 10, _fields: 'id,title,url' }, + headers: { 'X-WP-Nonce': cfg.restNonce }, + } ).done( function ( items ) { + $results.empty().show(); + if ( ! items.length ) { + $results.append( '
No results
' ); + return; + } + items.forEach( function ( item ) { + $( '
' ) + .text( item.title.rendered || item.title ) + .data( 'item', item ) + .on( 'click', function () { + onSelect( item ); + $results.hide(); + $input.val( '' ); + } ) + .appendTo( $results ); + } ); + } ); + }, 300 ); + } ); + $( document ).on( 'click', function ( e ) { + if ( ! $input.is( e.target ) ) { $results.hide(); } + } ); + } + + // Exclude search + if ( $( '#brezngeo-ls-exclude-search' ).length ) { + initPostSearch( + $( '#brezngeo-ls-exclude-search' ), + $( '#brezngeo-ls-exclude-results' ), + function ( item ) { + var id = item.id; + var title = item.title.rendered || item.title; + var fieldName = 'brezngeo_link_suggest_settings[excluded_posts][]'; + $( '#brezngeo-ls-excluded-list' ).append( + $( '' ) + .data( 'id', id ) + .append( + $( '' ).text( title ), + $( '' ).attr( 'name', fieldName ).val( id ), + $( '' ) + ) + ); + } + ); + } + + // Boost search + if ( $( '#brezngeo-ls-boost-search' ).length ) { + initPostSearch( + $( '#brezngeo-ls-boost-search' ), + $( '#brezngeo-ls-boost-results' ), + function ( item ) { + var id = item.id; + var title = item.title.rendered || item.title; + var idx = $( '.brezngeo-ls-boost-row' ).length; + var base = 'brezngeo_link_suggest_settings[boosted_posts][' + idx + ']'; + $( '#brezngeo-ls-boosted-list' ).append( + $( '
' ).append( + $( '' ).text( '\u2605 ' + title ), + $( '' ).attr( 'name', base + '[id]' ).val( id ), + $( '