PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.10.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.10.0
2.10.0 2.9.0 2.8.0 2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 All 51 releases
thinkrank / includes / abilities / settings / class-update-llms-txt-settings.php

class-update-llms-txt-settings.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.10.0, at includes/abilities/settings/class-update-llms-txt-settings.php

217 lines 6.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Update llms.txt settings ability.
4 *
5 * @package ThinkRank\Abilities\Settings
6 */
7
8 declare(strict_types=1);
9
10 namespace ThinkRank\Abilities\Settings;
11
12 use ThinkRank\Abilities\Ability_Base;
13 use ThinkRank\SEO\LLMs_Txt_Manager;
14
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit; // Exit if accessed directly.
17 }
18
19 /**
20 * Updates ThinkRank llms.txt settings.
21 *
22 * These settings feed the AI-oriented llms.txt file describing the site to
23 * language models, persisted through the LLMs_Txt_Manager SEO manager.
24 */
25 class Update_Llms_Txt_Settings extends Ability_Base {
26 /**
27 * Boolean-typed llms.txt keys.
28 */
29 private const BOOL_KEYS = [
30 'enabled',
31 'auto_generate',
32 ];
33
34 /**
35 * String-typed llms.txt keys (multi-line content preserved).
36 */
37 private const STRING_KEYS = [
38 'delivery_mode',
39 'site_name',
40 'website_description',
41 'key_features',
42 'target_audience',
43 'business_type',
44 'technical_stack',
45 'development_approach',
46 'setup_instructions',
47 'ai_context_custom',
48 'documentation_links',
49 'technical_links',
50 'optional_links',
51 'custom_sections',
52 ];
53
54 /**
55 * Constructor.
56 */
57 public function __construct() {
58 $this->id = 'thinkrank/update-llms-txt-settings';
59 $this->label = __( 'Update ThinkRank llms.txt Settings', 'thinkrank' );
60 $this->description = __( 'Update ThinkRank llms.txt settings, which describe the site to AI language models via the llms.txt file. Read the current values with get-llms-txt-settings first, then publish-llms-txt to write the file.', 'thinkrank' );
61 }
62
63 /**
64 * {@inheritDoc}
65 *
66 * @return array<string, bool|float|string>
67 */
68 public function get_annotations() {
69 return [
70 'readonly' => false,
71 // Settings writes are recoverable: the matching get-* ability reads
72 // the previous value, so nothing is lost that cannot be put back.
73 // `destructive` is reserved for calls that lose data or reach
74 // outside the site, and marking routine configuration with it made
75 // MCP clients demand a human approval for every save — which users
76 // reported as a permission bug, because the client's refusal reads
77 // as "No approval received" (#675).
78 'destructive' => false,
79 'idempotent' => true,
80 'priority' => 2.0,
81 'openWorldHint' => false,
82 ];
83 }
84
85 /**
86 * Build the JSON schema properties for llms.txt settings.
87 *
88 * @return array<string, mixed>
89 */
90 private static function schema_properties() {
91 $props = [];
92
93 foreach ( self::BOOL_KEYS as $key ) {
94 $props[ $key ] = [ 'type' => 'boolean' ];
95 }
96 foreach ( self::STRING_KEYS as $key ) {
97 $props[ $key ] = [ 'type' => 'string' ];
98 }
99
100 $props['delivery_mode']['enum'] = [ 'auto', 'static', 'dynamic' ];
101 $props['delivery_mode']['description'] = __( 'How /llms.txt is served: "static" writes a physical file the web server answers, "dynamic" keeps the document in WordPress and serves it from PHP as UTF-8, "auto" prefers static on Apache/LiteSpeed but falls back to dynamic when the published file turns out to be served without a character set.', 'thinkrank' );
102
103 // The delimiter is not guessable from the field name, and getting it
104 // wrong publishes mangled bullets to the file AI crawlers read (#765).
105 $props['key_features']['description'] = __( 'Key features of the site, one per line. A newline separates features; a comma is treated as part of a feature, so "Collect reviews from Trustpilot, Google, and Etsy" stays one feature rather than becoming three.', 'thinkrank' );
106
107 return $props;
108 }
109
110 /**
111 * {@inheritDoc}
112 *
113 * @return array<string, mixed>
114 */
115 public function get_input_schema() {
116 return [
117 'type' => 'object',
118 'additionalProperties' => false,
119 'properties' => [
120 'settings' => [
121 'type' => 'object',
122 'description' => __( 'llms.txt settings to update.', 'thinkrank' ),
123 'properties' => self::schema_properties(),
124 ],
125 ],
126 'required' => [ 'settings' ],
127 ];
128 }
129
130 /**
131 * {@inheritDoc}
132 *
133 * @return array<string, mixed>
134 */
135 public function get_output_schema() {
136 return [
137 'type' => 'object',
138 'properties' => [
139 'success' => [ 'type' => 'boolean' ],
140 'message' => [ 'type' => 'string' ],
141 ],
142 ];
143 }
144
145 /**
146 * Execute ability.
147 *
148 * @param array<string, mixed> $input Ability input payload.
149 * @return array<string, mixed>|\WP_Error
150 */
151 public function execute( $input ) {
152 $settings = isset( $input['settings'] ) && is_array( $input['settings'] ) ? $input['settings'] : [];
153
154 if ( empty( $settings ) ) {
155 return new \WP_Error(
156 'thinkrank_missing_llms_txt_settings_payload',
157 __( 'A llms.txt settings payload is required.', 'thinkrank' ),
158 [ 'status' => 400 ]
159 );
160 }
161
162 $mgr = new LLMs_Txt_Manager();
163 $merged = $mgr->get_settings( 'site', null );
164 $found = false;
165
166 foreach ( self::BOOL_KEYS as $key ) {
167 if ( array_key_exists( $key, $settings ) ) {
168 $merged[ $key ] = (bool) $settings[ $key ];
169 $found = true;
170 }
171 }
172 foreach ( self::STRING_KEYS as $key ) {
173 if ( array_key_exists( $key, $settings ) ) {
174 $merged[ $key ] = sanitize_textarea_field( (string) $settings[ $key ] );
175 $found = true;
176 }
177 }
178
179 if ( ! $found ) {
180 return new \WP_Error(
181 'thinkrank_no_valid_llms_txt_setting_keys',
182 __( 'No valid llms.txt setting keys were provided.', 'thinkrank' ),
183 [ 'status' => 400 ]
184 );
185 }
186
187 $result = $mgr->save_settings( 'site', null, $merged );
188
189 // A disable-save may persist the settings yet fail to remove the published
190 // file, so surface that partial failure instead of reporting plain success.
191 if ( $result && $mgr->unpublish_failed() ) {
192 return [
193 'success' => true,
194 'file_unpublished' => false,
195 'message' => __( 'Settings were saved, but the published llms.txt file could not be removed and may still be served. Please remove it manually.', 'thinkrank' ),
196 ];
197 }
198
199 // Likewise for a delivery-mode switch that could not move the published
200 // document — /llms.txt is still on the old path.
201 $delivery_warning = $mgr->delivery_switch_warning();
202 if ( $result && '' !== $delivery_warning ) {
203 return [
204 'success' => true,
205 'message' => $delivery_warning,
206 ];
207 }
208
209 return [
210 'success' => (bool) $result,
211 'message' => (bool) $result
212 ? __( 'llms.txt settings updated.', 'thinkrank' )
213 : __( 'Failed to update llms.txt settings.', 'thinkrank' ),
214 ];
215 }
216 }
217