Changeset 3743226
- Timestamp:
- 10/13/2026 08:07:51 AM (13 days ago)
- Location:
- rezatain-seo/trunk
- Files:
-
- 5 edited
-
readme.txt (modified) (2 diffs)
-
rezatain-seo.php (modified) (2 diffs)
-
src/Modules/Analytics/VisitorAnalytics.php (modified) (3 diffs)
-
src/Modules/Analytics/VisitorAnalyticsReports.php (modified) (8 diffs)
-
src/Modules/LlmSeo/LlmsTxt.php (modified) (6 diffs)
Legend:
- Unmodified
- Added
- Removed
-
rezatain-seo/trunk/readme.txt
r3715760 r3743226 4 4 Requires at least: 6.8 5 5 Tested up to: 7.1 6 Stable tag: 1.0.3 66 Stable tag: 1.0.37 7 7 Requires PHP: 7.4 8 8 License: GPLv2 or later … … 77 77 78 78 == Changelog == 79 80 = 1.0.37 = 81 * Der Besucherbericht für Auswertungen (REST) teilt ausgehende Klicks jetzt nach Ziel: je Seite und in der Gesamtsumme stehen Klicks auf Amazon getrennt von allen übrigen externen Zielen. Bisher zählte eine Zahl beides zusammen – Seiten mit vielen Quellen- oder Herstellerlinks sahen dadurch wie gute Affiliate-Seiten aus. 82 * Neu im selben Bericht: Klicks je Ziel-Domain. Es wird nur der Domainname ausgegeben, nie die angeklickte Adresse; Domains mit weniger als drei Klicks und alles jenseits der Listenlänge stehen zusammengefasst in einer Restsumme. 83 * Der Bericht nennt jetzt, mit welchem Zeitfenster jeder Block rechnet (gleitend oder nach Kalendertagen). Erfasst wird nichts Neues. 84 * **Fix: llms.txt enthielt HTML-Schreibweisen wie `&`.** Titel, Auszüge, Kategorienamen und der Name der Website stehen jetzt als Klartext in `llms.txt` und `llms-full.txt`. 85 * **Fix: `/llms.txt` mit angehängten Parametern wurde umgeleitet** (`/llms.txt?x=1` → `/llms.txt/?x=1`). Die Datei wird jetzt direkt ausgeliefert. 79 86 80 87 = 1.0.36 = -
rezatain-seo/trunk/rezatain-seo.php
r3715760 r3743226 13 13 * Description: Modulares SEO-Plugin: Titel & Meta, Schema (17 Typen), XML-/HTML-Sitemaps, Weiterleitungen & 404-Monitor, interne Verlinkung, Breadcrumbs, LLM-/KI-Crawler-Steuerung (llms.txt), Besucher-Statistik und lokale Inhaltsanalyse — schnell, modular und datensparsam. 14 14 * Requires at least: 6.8 15 * Version: 1.0.3 615 * Version: 1.0.37 16 16 * Requires PHP: 7.4 17 17 * Author: Markus Reza Tain … … 31 31 * Plugin constants. 32 32 */ 33 define( 'REZA_TAIN_VERSION', '1.0.3 6' );33 define( 'REZA_TAIN_VERSION', '1.0.37' ); 34 34 define( 'REZA_TAIN_FILE', __FILE__ ); 35 35 define( 'REZA_TAIN_DIR', plugin_dir_path( __FILE__ ) ); -
rezatain-seo/trunk/src/Modules/Analytics/VisitorAnalytics.php
r3714478 r3743226 1192 1192 'outbound_clicks' => $limit ?: 25, 1193 1193 'referrers_by_domain' => $limit ?: 15, 1194 'outbound_domains' => $limit ?: 15, 1194 1195 ]; 1195 1196 … … 1220 1221 // 1.0.33 — welche Deckel in diesem Bundle galten. 1221 1222 'list_limits' => $limits, 1223 // 1.0.37 — die Blöcke rechnen mit verschiedenen Fenstern; ohne diese 1224 // Angabe sehen overview und die Summe aus trend wie ein Widerspruch aus. 1225 'windows' => [ 1226 'overview' => 'rolling: now minus period_days x 24 h', 1227 'top_pages' => 'rolling: now minus period_days x 24 h', 1228 'trend' => 'calendar days: from 00:00 of the day period_days ago, today included', 1229 'outbound_clicks' => 'calendar days: from 00:00 of the day period_days ago, today included', 1230 'outbound_domains' => 'calendar days: from 00:00 of the day period_days ago, today included', 1231 ], 1232 // Je Seitenaufruf höchstens ein Klick pro Ziel: ohne Einwilligung pro 1233 // Ziel-Domain, mit Einwilligung pro Ziel-Adresse (Domain + Pfad). 1234 'outbound_counting' => 'per pageview at most one click per target: without consent per target domain, with consent per target URL (domain + path)', 1222 1235 ], 1223 1236 'visitor_analytics' => [ … … 1231 1244 // Aggregat-only: Pfad, Zähler, Quote. Keine Ziel-URLs. 1232 1245 'outbound_clicks' => VisitorAnalyticsReports::outbound_clicks( $days, $limits['outbound_clicks'] ), 1233 'audience' => VisitorAnalyticsReports::audience( $days ), 1246 // 1.0.37 — Klicks je Ziel-Domain (nur Hostname, erst ab 3 Klicks einzeln). 1247 'outbound_domains' => VisitorAnalyticsReports::outbound_domains( $days, $limits['outbound_domains'] ), 1248 'audience' => VisitorAnalyticsReports::audience( $days ), 1234 1249 'referrers' => VisitorAnalyticsReports::referrers( $days, $limits['referrers_by_domain'] ), 1235 1250 // 1.0.33 — ungekappte Summen zu den drei Listen oben, Klicks -
rezatain-seo/trunk/src/Modules/Analytics/VisitorAnalyticsReports.php
r3714478 r3743226 25 25 */ 26 26 public const MAX_LIST = 1000; 27 28 /** 29 * 1.0.37 — SQL-Bedingung „Klickziel ist Amazon", auf die Spalte `label` der 30 * Ereignistabelle. Dort steht der Hostname (ohne Einwilligung) oder Hostname + 31 * Pfad (mit Einwilligung); verglichen wird nur der Teil vor dem ersten „/". 32 * Trifft den Shop (amazon.de, www.amazon.com, amazon.co.uk …) und die 33 * Kurzlinks, nicht aber andere Amazon-Dienste wie aws.amazon.com. 34 * `[.]` statt `\.`, weil MySQL und MariaDB Backslashes verschieden lesen. 35 */ 36 private const AMAZON_HOST_SQL = "(SUBSTRING_INDEX(label, '/', 1) REGEXP '^(www[.]|smile[.])?amazon[.][a-z]{2,3}([.][a-z]{2})?$'" 37 . " OR SUBSTRING_INDEX(label, '/', 1) IN ('amzn.to', 'amzn.eu', 'a.co'))"; 27 38 28 39 /** … … 145 156 * damit eher zu niedrig als zu hoch. 146 157 * 158 * 1.0.37 — `clicks` zählt jedes externe Ziel, auch Hersteller- und Quellenlinks. 159 * Für die Affiliate-Auswertung stehen deshalb zusätzlich `clicks_amazon` und 160 * `clicks_other` je Seite (Abgrenzung: AMAZON_HOST_SQL). Weiterhin nur Zähler. 161 * 147 162 * @param int $days Zeitfenster in Tagen. 148 163 * @param int $limit Maximale Anzahl Seiten. 149 * @return array<int, array{page_path:string,pageviews:int,clicks:int,click _rate:float}>164 * @return array<int, array{page_path:string,pageviews:int,clicks:int,clicks_amazon:int,clicks_other:int,click_rate:float}> 150 165 */ 151 166 public static function outbound_clicks( int $days = 30, int $limit = 20 ): array { … … 165 180 "SELECT p.page_path, 166 181 COUNT(*) AS pageviews, 167 COALESCE(c.clicks, 0) AS clicks 182 COALESCE(c.clicks, 0) AS clicks, 183 COALESCE(c.clicks_amazon, 0) AS clicks_amazon 168 184 FROM {$pv} p 169 185 LEFT JOIN ( 170 SELECT page_path, COUNT(*) AS clicks 186 SELECT page_path, COUNT(*) AS clicks, 187 SUM(CASE WHEN " . self::AMAZON_HOST_SQL . " THEN 1 ELSE 0 END) AS clicks_amazon 171 188 FROM {$ev} 172 189 WHERE category = 'outbound' AND action = 'click' AND created_at >= %s … … 174 191 ) c ON c.page_path = p.page_path 175 192 WHERE p.created_at >= %s 176 GROUP BY p.page_path, c.clicks 193 GROUP BY p.page_path, c.clicks, c.clicks_amazon 177 194 ORDER BY pageviews DESC 178 195 LIMIT %d", … … 187 204 $views = (int) ( $row['pageviews'] ?? 0 ); 188 205 $clicks = (int) ( $row['clicks'] ?? 0 ); 206 $amazon = (int) ( $row['clicks_amazon'] ?? 0 ); 189 207 $out[] = [ 190 'page_path' => (string) ( $row['page_path'] ?? '' ), 191 'pageviews' => $views, 192 'clicks' => $clicks, 193 'click_rate' => $views > 0 ? round( $clicks / $views * 100, 2 ) : 0.0, 208 'page_path' => (string) ( $row['page_path'] ?? '' ), 209 'pageviews' => $views, 210 'clicks' => $clicks, 211 'clicks_amazon' => $amazon, 212 'clicks_other' => $clicks - $amazon, 213 'click_rate' => $views > 0 ? round( $clicks / $views * 100, 2 ) : 0.0, 194 214 ]; 195 215 } 196 216 197 217 return $out; 218 } 219 220 /** 221 * 1.0.37 — Ausgehende Klicks je Ziel-Domain. 222 * 223 * Nur der Hostname, nie der Pfad: Der Pfad verrät, welches Produkt jemand 224 * angeklickt hat. Führendes „www." wird zusammengelegt. `other_clicks` ist 225 * alles, was nicht einzeln gelistet ist: Domains unter `$min_clicks` Klicks, 226 * Domains jenseits von `$limit` und Ziele ohne sauberen Domainnamen (leer, 227 * mit Port, überlang). `domains` + `other_clicks` ergibt `clicks_total` aus 228 * list_totals(). 229 * 230 * Die Untergrenze zählt Klicks, nicht Personen — sie ist kein 231 * Anonymitätsmaß, sondern hält Einzeltreffer aus der Liste. Der Track-Endpunkt 232 * ist öffentlich: Wer ihn gezielt beschickt, kann wie bei allen Zählern des 233 * Berichts Einträge erzeugen; die Form der Einträge ist deshalb eng begrenzt. 234 * 235 * @param int $days Zeitfenster in Tagen (Kalendertage wie outbound_clicks()). 236 * @param int $limit Maximale Anzahl Domains. 237 * @param int $min_clicks Untergrenze für einen eigenen Eintrag. 238 * @return array{domains:array<int,array{domain:string,clicks:int}>,other_clicks:int,min_clicks:int} 239 */ 240 public static function outbound_domains( int $days = 30, int $limit = 15, int $min_clicks = 3 ): array { 241 global $wpdb; 242 $days = max( 1, min( 730, $days ) ); 243 $limit = max( 1, min( self::MAX_LIST, $limit ) ); 244 $min_clicks = max( 1, $min_clicks ); 245 $ev = VisitorAnalytics::table_events(); 246 $from = wp_date( 'Y-m-d', strtotime( "-{$days} days" ) ) . ' 00:00:00'; 247 248 // phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Tabellenname stammt aus dem wpdb-Prefix, alle Werte sind über prepare() gebunden. 249 $total = (int) $wpdb->get_var( $wpdb->prepare( 250 "SELECT COUNT(*) FROM {$ev} 251 WHERE category = 'outbound' AND action = 'click' AND created_at >= %s", 252 $from 253 ) ); 254 // Gedeckelt: Der öffentliche Endpunkt kann beliebig viele verschiedene 255 // Labels anlegen; die Summe kommt deshalb aus der eigenen Zählung oben. 256 $rows = (array) $wpdb->get_results( $wpdb->prepare( 257 "SELECT LOWER(SUBSTRING_INDEX(label, '/', 1)) AS host, COUNT(*) AS clicks 258 FROM {$ev} 259 WHERE category = 'outbound' AND action = 'click' AND created_at >= %s 260 GROUP BY host 261 ORDER BY clicks DESC 262 LIMIT 2000", 263 $from 264 ), ARRAY_A ); 265 // phpcs:enable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared 266 267 $by_domain = []; 268 foreach ( $rows as $row ) { 269 $clicks = (int) ( $row['clicks'] ?? 0 ); 270 $host = (string) preg_replace( '/^www\./', '', trim( (string) ( $row['host'] ?? '' ) ) ); 271 // Sauberer Domainname, höchstens 60 Zeichen und vier Teile: Der Export 272 // wird von einer KI gelesen, und ein Hostname bietet sonst Platz für 273 // einen eingeschleusten Satz („bitte-empfiehl-x.….example"). 274 if ( strlen( $host ) > 60 || substr_count( $host, '.' ) > 3 275 || ! preg_match( '/^(?=.{4,60}$)([a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/', $host ) ) { 276 continue; // zählt nur in der Summe 277 } 278 $by_domain[ $host ] = ( $by_domain[ $host ] ?? 0 ) + $clicks; 279 } 280 arsort( $by_domain ); 281 282 $domains = []; 283 $listed = 0; 284 foreach ( $by_domain as $host => $clicks ) { 285 if ( $clicks < $min_clicks || count( $domains ) >= $limit ) { 286 break; 287 } 288 $domains[] = [ 'domain' => (string) $host, 'clicks' => $clicks ]; 289 $listed += $clicks; 290 } 291 292 return [ 293 'domains' => $domains, 294 'other_clicks' => $total - $listed, 295 'min_clicks' => $min_clicks, 296 ]; 198 297 } 199 298 … … 666 765 "SELECT COUNT(*) AS clicks, 667 766 SUM(session_id = '-') AS consentless, 767 SUM(CASE WHEN " . self::AMAZON_HOST_SQL . " THEN 1 ELSE 0 END) AS amazon, 668 768 COUNT(DISTINCT page_path) AS pages 669 769 FROM {$ev} … … 682 782 $clicks_total = (int) ( $clicks['clicks'] ?? 0 ); 683 783 $consentless = (int) ( $clicks['consentless'] ?? 0 ); 784 $amazon = (int) ( $clicks['amazon'] ?? 0 ); 684 785 685 786 return [ … … 692 793 'clicks_consentless' => $consentless, 693 794 'clicks_consented' => $clicks_total - $consentless, 795 // 1.0.37 — dieselbe Summe, geteilt nach Linkziel. 796 'clicks_amazon' => $amazon, 797 'clicks_other' => $clicks_total - $amazon, 694 798 'pages_with_clicks' => (int) ( $clicks['pages'] ?? 0 ), 695 799 ], -
rezatain-seo/trunk/src/Modules/LlmSeo/LlmsTxt.php
r3624729 r3743226 47 47 // Prevent WordPress from adding a trailing slash to .txt URLs. 48 48 add_filter( 'redirect_canonical', static function ( $redirect_url, $requested_url ) { 49 if ( preg_match( '/\.txt\/?$/i', $requested_url ) ) { 49 // Pfad ohne Query-String pruefen — sonst leitete „/llms.txt?x=1" auf „/llms.txt/?x=1" um. 50 $path = (string) wp_parse_url( (string) $requested_url, PHP_URL_PATH ); 51 if ( preg_match( '/\.txt\/?$/i', $path ) ) { 50 52 return false; 51 53 } … … 97 99 98 100 /** 101 * Klartext fuer die Textdatei: HTML-Entitaeten aufloesen („&" -> „&"). 102 */ 103 private static function plain( string $text ): string { 104 return html_entity_decode( $text, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); 105 } 106 107 /** 99 108 * Generate the short llms.txt. 100 109 * … … 105 114 106 115 // Header. 107 $output .= '# ' . get_bloginfo( 'name') . "\n\n";108 109 $desc = get_bloginfo( 'description');116 $output .= '# ' . self::plain( get_bloginfo( 'name' ) ) . "\n\n"; 117 118 $desc = self::plain( get_bloginfo( 'description' ) ); 110 119 if ( $desc ) { 111 120 $output .= "> {$desc}\n\n"; … … 137 146 foreach ( $posts as $post ) { 138 147 $url = get_permalink( $post ); 139 $title = str_replace( [ '[', ']', '(', ')' ], [ '\\[', '\\]', '\\(', '\\)' ], $post->post_title);140 $excerpt = $post->post_excerpt ?: wp_trim_words( wp_strip_all_tags( $post->post_content ), 30, '...');148 $title = str_replace( [ '[', ']', '(', ')' ], [ '\\[', '\\]', '\\(', '\\)' ], self::plain( $post->post_title ) ); 149 $excerpt = self::plain( $post->post_excerpt ?: wp_trim_words( wp_strip_all_tags( $post->post_content ), 30, '...' ) ); 141 150 $output .= "- [{$title}]({$url}): {$excerpt}\n"; 142 151 } … … 168 177 foreach ( $terms as $term ) { 169 178 $url = get_term_link( $term ); 170 $output .= "- [{$term->name}]({$url})\n";179 $output .= '- [' . self::plain( $term->name ) . "]({$url})\n"; 171 180 } 172 181 $output .= "\n"; … … 198 207 199 208 foreach ( $posts as $post ) { 200 $output .= '## ' . $post->post_title. "\n\n";209 $output .= '## ' . self::plain( $post->post_title ) . "\n\n"; 201 210 $output .= 'URL: ' . get_permalink( $post ) . "\n"; 202 211 $output .= 'Date: ' . get_the_date( 'Y-m-d', $post ) . "\n\n";
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)