PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.13.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.13.0
2.14.3 2.14.2 2.14.1 2.14.0 2.13.0 2.12.0 2.11.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 All 58 releases
← All changes | includes/admin/importers/class-snapshot-migrator.php +383 -20 2.10.0 → 2.13.0 View file →
@@ -54,8 +54,15 @@
54 54 'twitter_description' => '_thinkrank_twitter_description',
55 55 'twitter_image' => '_thinkrank_twitter_image',
56 56 'primary_category' => '_thinkrank_primary_category',
57 57 'schema_type' => '_thinkrank_selected_schema_type',
58 + // WooCommerce product identifier (GTIN/MPN/ISBN). Free does not read
59 + // it; ThinkRank Pro's Product_Fields does, under this exact key, so
60 + // importing it here means the identifier is already in place when Pro
61 + // is activated. Without it Google reports "missing identifier" on every
62 + // product after a switch, which is a rich-result warning the user did
63 + // not have before they migrated (#715).
64 + 'product_identifier' => '_thinkrank_product_gtin',
58 65 ];
59 66
60 67 /**
61 68 * Canonical robots meta fields. Composed into JSON-encoded
@@ -104,8 +111,15 @@
104 111 'image_seo',
105 112 'sitemap_settings',
106 113 'analytics_connected',
107 114 'focus_pages',
115 + // Applied by migrate_site_identity() (#886).
116 + 'robots_txt',
117 + 'feed',
118 + // Applied by migrate_markdown_for_ai() into ThinkRank Pro (#886).
119 + // Without Pro it has no home, but neither is anything lost: cleanup
120 + // removes a feature toggle, not content.
121 + 'markdown_for_ai',
108 122 // Capture-all raw buckets (whole source option sets stored verbatim).
109 123 // They live in the SNAPSHOT — cleanup never touches the snapshot — and
110 124 // a re-export recreates them, so they never block cleanup.
111 125 'raw_options',
@@ -367,8 +381,15 @@
367 381 if ($object_type === 'post' && $this->migrate_video_schema($object_id, $data, $record)) {
368 382 $record_had_writes = true;
369 383 }
370 384
385 + // The post's own schemas as ThinkRank Pro Custom Schema entries
386 + // scoped to it (extended.custom_schemas, #886).
387 + if ($object_type === 'post' && !empty($record['extended']['custom_schemas']) && is_array($record['extended']['custom_schemas'])
388 + && $this->migrate_custom_schemas($source_plugin, $record['extended']['custom_schemas']) > 0) {
389 + $record_had_writes = true;
390 + }
391 +
371 392 // Per-object redirect. SEOPress is the one source that stores a
372 393 // redirect as object meta rather than in a rules table, so its
373 394 // per-post redirects were exported into extended.redirect_* and
374 395 // then dropped for want of anywhere to put them. They have a home
@@ -1758,9 +1779,9 @@
1758 1779 $processed++;
1759 1780 }
1760 1781 }
1761 1782
1762 - if (!empty($data['homepage_title']) || !empty($data['homepage_description']) || !empty($data['organization_name']) || !empty($data['organization_logo'])) {
1783 + if (!empty($data['homepage_title']) || !empty($data['organization_name']) || !empty($data['organization_logo'])) {
1763 1784 $site_identity = get_option('thinkrank_site_identity_settings', []);
1764 1785 $updated = false;
1765 1786
1766 1787 if (!empty($data['homepage_title']) && empty($site_identity['homepage_title'])) {
@@ -1766,12 +1787,8 @@
1766 1787 if (!empty($data['homepage_title']) && empty($site_identity['homepage_title'])) {
1767 1788 $site_identity['homepage_title'] = $data['homepage_title'];
1768 1789 $updated = true;
1769 1790 }
1770 - if (!empty($data['homepage_description']) && empty($site_identity['homepage_description'])) {
1771 - $site_identity['homepage_description'] = $data['homepage_description'];
1772 - $updated = true;
1773 - }
1774 1791 if (!empty($data['organization_name']) && empty($site_identity['organization_name'])) {
1775 1792 $site_identity['organization_name'] = $data['organization_name'];
1776 1793 $updated = true;
1777 1794 }
@@ -1785,21 +1802,10 @@
1785 1802 $processed++;
1786 1803 }
1787 1804 }
1788 1805
1789 - if (!empty($data['social_profiles'])) {
1790 - $social = get_option('thinkrank_social_media_settings', []);
1791 - $updated = false;
1792 -
1793 - foreach ($data['social_profiles'] as $platform => $url) {
1794 - if (!empty($url) && empty($social[$platform])) {
1795 - $social[$platform] = $url;
1796 - $updated = true;
1797 - }
1798 - }
1799 -
1800 - if ($updated) {
1801 - update_option('thinkrank_social_media_settings', $social);
1806 + if (!empty($data['social_profiles']) && is_array($data['social_profiles'])) {
1807 + if ($this->migrate_social_profiles($data['social_profiles'])) {
1802 1808 $processed++;
1803 1809 }
1804 1810 }
1805 1811
@@ -1856,8 +1862,23 @@
1856 1862 $processed++;
1857 1863 }
1858 1864 }
1859 1865
1866 + // Per-taxonomy and blog-index noindex, into the Content Type Matrix.
1867 + // The templates beside them have no ThinkRank store and stay preserved.
1868 + if ($this->migrate_archive_noindex($extended)) {
1869 + $processed++;
1870 + }
1871 +
1872 + // Site-wide custom schemas and Markdown for AI, both ThinkRank Pro.
1873 + $global_schemas = $extended['custom_schemas']['entries'] ?? [];
1874 + if (is_array($global_schemas) && $this->migrate_custom_schemas($plugin, $global_schemas) > 0) {
1875 + $processed++;
1876 + }
1877 + if ($this->migrate_markdown_for_ai($extended)) {
1878 + $processed++;
1879 + }
1880 +
1860 1881 // Site-identity settings (homepage/org/breadcrumbs/local SEO) are served to
1861 1882 // the frontend from the wp_thinkrank_seo_settings table via the manager, not
1862 1883 // from the option written above — route them through the manager so they
1863 1884 // actually take effect.
@@ -1993,8 +2014,12 @@
1993 2014 // real template and beats the literal-resolved data.homepage_title, so it
1994 2015 // wins when the source provided one.
1995 2016 $title_formats = is_array($extended['title_formats'] ?? null) ? $extended['title_formats'] : [];
1996 2017 $set('homepage_title', $title_formats['homepage_title'] ?? ($data['homepage_title'] ?? ''));
2018 + // The homepage meta description. It used to go to the legacy
2019 + // thinkrank_site_identity_settings option, which nothing reads, so the
2020 + // homepage fell back to the tagline (#897).
2021 + $set('homepage_description', trim((string) ($data['homepage_description'] ?? '')));
1997 2022 $set('site_name', $data['organization_name'] ?? '');
1998 2023 $set('alternate_name', $data['alternate_name'] ?? '');
1999 2024 $set('logo_url', $data['organization_logo'] ?? '');
2000 2025
@@ -2036,8 +2061,23 @@
2036 2061 $set('breadcrumb_separator', $breadcrumbs['separator'] ?? '');
2037 2062 $set('breadcrumb_prefix', $breadcrumbs['prefix'] ?? '');
2038 2063 }
2039 2064
2065 + // A hand-written robots.txt (extended.robots_txt.content) goes into
2066 + // ThinkRank's Robots.txt textarea only while that is still empty:
2067 + // a file the user already wrote here is never replaced (#886).
2068 + $robots_txt = $extended['robots_txt'] ?? [];
2069 + if (is_array($robots_txt)) {
2070 + $set('robots_txt_content', trim((string) ($robots_txt['content'] ?? '')));
2071 + }
2072 +
2073 + // The "appeared first on" feed backlink (extended.feed.source_link).
2074 + // Only switches it on: a source that had it off is no reason to turn
2075 + // off a signature ThinkRank seeds for new installs.
2076 + if (!empty($extended['feed']['source_link']) && empty($current['feed_source_link'])) {
2077 + $updates['feed_source_link'] = true;
2078 + }
2079 +
2040 2080 // Local SEO (extended.local_seo) — migrate the full NAP + geo when there
2041 2081 // is any meaningful business data (name, phone, address or coordinates),
2042 2082 // and enable the feature alongside it. Each field is written only while
2043 2083 // ThinkRank's Business Info still holds its default (non-destructive).
@@ -2184,9 +2224,10 @@
2184 2224 }
2185 2225
2186 2226 $app_id = trim((string) ($defaults['facebook_app_id'] ?? ''));
2187 2227 $og_image = trim((string) ($defaults['og_default_image'] ?? ''));
2188 - if ($app_id === '' && $og_image === '') {
2228 + $twitter_image = trim((string) ($defaults['twitter_default_image'] ?? ''));
2229 + if ($app_id === '' && $og_image === '' && $twitter_image === '') {
2189 2230 return false;
2190 2231 }
2191 2232
2192 2233 $manager = new \ThinkRank\SEO\Social_Meta_Manager();
@@ -2202,8 +2243,12 @@
2202 2243 // outside the allow-list never reaches the database.
2203 2244 if ($og_image !== '' && empty($current['default_og_image'])) {
2204 2245 $updates['default_og_image'] = $og_image;
2205 2246 }
2247 + // The Twitter card fallback image (Slim SEO's default_twitter_image).
2248 + if ($twitter_image !== '' && empty($current['default_twitter_image'])) {
2249 + $updates['default_twitter_image'] = $twitter_image;
2250 + }
2206 2251
2207 2252 if (empty($updates)) {
2208 2253 return false;
2209 2254 }
@@ -2211,8 +2256,97 @@
2211 2256 return (bool) $manager->save_settings('site', null, $updates);
2212 2257 }
2213 2258
2214 2259 /**
2260 + * Social profile URLs (data.social_profiles, platform => URL) into the
2261 + * stores that are read (#898).
2262 + *
2263 + * They used to be written as `thinkrank_social_media_settings[<platform>]`,
2264 + * a key nothing reads: the consumer looks for `<platform>_url`, the
2265 + * Organization graph reads the Schema Manager's
2266 + * `organization_social_<platform>`, and `twitter:site` comes from the
2267 + * Social Meta `twitter_username`. So the profiles never reached `sameAs`
2268 + * and the X handle stopped printing once the source plugin was off. Each
2269 + * store is filled only where it is still empty.
2270 + *
2271 + * @param array $profiles platform => profile URL
2272 + * @return bool True if anything was written
2273 + */
2274 + private function migrate_social_profiles(array $profiles): bool {
2275 + $profiles = array_filter(array_map(
2276 + static fn($url): string => is_scalar($url) ? trim((string) $url) : '',
2277 + $profiles
2278 + ));
2279 + if (empty($profiles)) {
2280 + return false;
2281 + }
2282 +
2283 + $written = false;
2284 +
2285 + $social = get_option('thinkrank_social_media_settings', []);
2286 + $social = is_array($social) ? $social : [];
2287 + $social_updated = false;
2288 + foreach ($profiles as $platform => $url) {
2289 + $key = sanitize_key((string) $platform) . '_url';
2290 + if (empty($social[$key])) {
2291 + $social[$key] = esc_url_raw($url);
2292 + $social_updated = true;
2293 + }
2294 + }
2295 + if ($social_updated) {
2296 + update_option('thinkrank_social_media_settings', $social);
2297 + $written = true;
2298 + }
2299 +
2300 + // The Organization node's sameAs.
2301 + $schema_platforms = ['facebook', 'twitter', 'linkedin', 'instagram', 'youtube', 'pinterest', 'whatsapp', 'telegram'];
2302 + $manager = $this->create_schema_manager();
2303 + if ($manager !== null) {
2304 + $current = $manager->get_settings('site');
2305 + $updates = [];
2306 + foreach ($profiles as $platform => $url) {
2307 + $field = 'organization_social_' . $platform;
2308 + if (in_array($platform, $schema_platforms, true) && filter_var($url, FILTER_VALIDATE_URL) && empty($current[$field])) {
2309 + $updates[$field] = esc_url_raw($url);
2310 + }
2311 + }
2312 + if (!empty($updates) && $manager->save_settings('site', null, $updates)) {
2313 + $written = true;
2314 + }
2315 + }
2316 +
2317 + // twitter:site, from an X/Twitter profile URL.
2318 + $handle = self::twitter_handle_from_url((string) ($profiles['twitter'] ?? ''));
2319 + if ($handle !== '' && class_exists('ThinkRank\\SEO\\Social_Meta_Manager')) {
2320 + $social_meta = new \ThinkRank\SEO\Social_Meta_Manager();
2321 + $current = $social_meta->get_settings('site');
2322 + if (empty($current['twitter_username']) && $social_meta->save_settings('site', null, ['twitter_username' => $handle])) {
2323 + $written = true;
2324 + }
2325 + }
2326 +
2327 + return $written;
2328 + }
2329 +
2330 + /**
2331 + * The handle in an x.com / twitter.com profile URL, or '' when the URL is
2332 + * not one (`https://x.com/arsenal` → `arsenal`). Handles are 1-15 letters,
2333 + * digits or underscores, which is also what Social Meta accepts.
2334 + *
2335 + * @param string $url Profile URL
2336 + */
2337 + public static function twitter_handle_from_url(string $url): string {
2338 + $host = strtolower((string) wp_parse_url($url, PHP_URL_HOST));
2339 + if (!in_array(preg_replace('/^(www\.|mobile\.)/', '', $host), ['x.com', 'twitter.com'], true)) {
2340 + return '';
2341 + }
2342 + $segment = explode('/', trim((string) wp_parse_url($url, PHP_URL_PATH), '/'))[0] ?? '';
2343 + $segment = ltrim(rawurldecode($segment), '@');
2344 +
2345 + return preg_match('/^[A-Za-z0-9_]{1,15}$/', $segment) ? $segment : '';
2346 + }
2347 +
2348 + /**
2215 2349 * Migrate the source plugin's Pinterest site-verification code into
2216 2350 * ThinkRank's core `pinterest_site_verification` setting. Never overwrites
2217 2351 * a configured code.
2218 2352 *
@@ -3059,11 +3193,39 @@
3059 3193 foreach ($extended as $key => $value) {
3060 3194 if (in_array($key, self::HANDLED_EXTENDED_SETTINGS, true) || empty($value)) {
3061 3195 continue;
3062 3196 }
3197 + // A bucket the migrator applies part of blocks cleanup only for
3198 + // the part it does not.
3199 + if (is_array($value) && empty($this->unapplied_remainder((string) $key, $value))) {
3200 + continue;
3201 + }
3202 + // Custom schemas have a home only in ThinkRank Pro. Without it
3203 + // they are preserved, and cleanup would delete the source's
3204 + // copy, including every post's own schemas.
3205 + if ($key === 'custom_schemas') {
3206 + if ($this->create_custom_schema_repository() !== null) {
3207 + continue;
3208 + }
3209 + $count = count(is_array($value['entries'] ?? null) ? $value['entries'] : []) + (int) ($value['post_count'] ?? 0);
3210 + $buckets[] = [
3211 + 'key' => 'settings.custom_schemas',
3212 + 'label' => (string) self::extended_bucket_label('custom_schemas'),
3213 + 'count' => max(1, $count),
3214 + ];
3215 + continue;
3216 + }
3217 + if ($key === 'schema_templates' && is_array($value)) {
3218 + $buckets[] = [
3219 + 'key' => 'settings.schema_templates',
3220 + 'label' => (string) self::extended_bucket_label('schema_templates'),
3221 + 'count' => count($value),
3222 + ];
3223 + continue;
3224 + }
3063 3225 $buckets[] = [
3064 3226 'key' => 'settings.' . $key,
3065 - 'label' => (string) $key,
3227 + 'label' => self::extended_bucket_label((string) $key) ?? ucfirst(str_replace('_', ' ', (string) $key)),
3066 3228 'count' => 1,
3067 3229 ];
3068 3230 }
3069 3231 }
@@ -3068,8 +3230,209 @@
3068 3230 }
3069 3231 }
3070 3232
3071 3233 return $buckets;
3234 + }
3235 +
3236 + /**
3237 + * Save canonical custom schema entries into ThinkRank Pro's Custom Schema.
3238 + *
3239 + * Each entry's id is derived from the source and its `key`, so running
3240 + * the migration again finds the entry it wrote and leaves it — including
3241 + * any edit made since. An entry the Repository refuses (an invalid
3242 + * condition, an oversized document) is skipped; the source still holds
3243 + * it, and the snapshot keeps the rendered JSON.
3244 + *
3245 + * @param string $plugin Source plugin slug
3246 + * @param array $entries Canonical entries {key, title, enabled, json, conditions}
3247 + * @return int Entries written
3248 + */
3249 + private function migrate_custom_schemas(string $plugin, array $entries): int {
3250 + $repository = $this->create_custom_schema_repository();
3251 + if ($repository === null) {
3252 + return 0;
3253 + }
3254 +
3255 + $written = 0;
3256 + foreach ($entries as $entry) {
3257 + if (!is_array($entry) || trim((string) ($entry['json'] ?? '')) === '') {
3258 + continue;
3259 + }
3260 + $id = 'cs_' . substr(md5($plugin . '|' . (string) ($entry['key'] ?? wp_json_encode($entry))), 0, 12);
3261 + if ($repository->get($id) !== null) {
3262 + continue;
3263 + }
3264 +
3265 + try {
3266 + $repository->save([
3267 + 'id' => $id,
3268 + 'title' => (string) ($entry['title'] ?? ''),
3269 + 'enabled' => !empty($entry['enabled']),
3270 + 'json' => (string) $entry['json'],
3271 + 'conditions' => is_array($entry['conditions'] ?? null) ? $entry['conditions'] : ['include' => [], 'exclude' => []],
3272 + ]);
3273 + $written++;
3274 + } catch (\Throwable $e) {
3275 + continue;
3276 + }
3277 + }
3278 +
3279 + return $written;
3280 + }
3281 +
3282 + /**
3283 + * ThinkRank Pro's Custom Schema repository, when Pro is active.
3284 + *
3285 + * @return object|null
3286 + */
3287 + protected function create_custom_schema_repository(): ?object {
3288 + return class_exists('ThinkRank\\Pro\\Schema\\Repository') ? new \ThinkRank\Pro\Schema\Repository() : null;
3289 + }
3290 +
3291 + /**
3292 + * Turn on ThinkRank Pro's Markdown for AI with the source's post types
3293 + * (extended.markdown_for_ai), while its settings were never saved.
3294 + *
3295 + * @param array $extended Canonical settings `extended` payload
3296 + * @return bool True if written
3297 + */
3298 + private function migrate_markdown_for_ai(array $extended): bool {
3299 + $markdown = $extended['markdown_for_ai'] ?? [];
3300 + if (!is_array($markdown) || empty($markdown['enabled']) || !class_exists('ThinkRank\\Pro\\Markdown_For_AI\\Settings')) {
3301 + return false;
3302 + }
3303 + if (get_option('thinkrank_pro_markdown_for_ai', null) !== null) {
3304 + return false;
3305 + }
3306 +
3307 + $types = array_values(array_filter(
3308 + array_map('strval', is_array($markdown['post_types'] ?? null) ? $markdown['post_types'] : ['post']),
3309 + 'post_type_exists'
3310 + ));
3311 +
3312 + return (bool) (new \ThinkRank\Pro\Markdown_For_AI\Settings())->save([
3313 + 'enabled' => true,
3314 + 'post_types' => !empty($types) ? $types : ['post'],
3315 + ]);
3316 + }
3317 +
3318 + /**
3319 + * What a preserved settings bucket holds, as the cleanup warning names it.
3320 + *
3321 + * The warning exists so someone can decide whether to delete their old
3322 + * plugin's data; printing `code_injection` does not help them decide
3323 + * (#900). Every bucket an exporter can leave unapplied has an entry here,
3324 + * and a test fails when one is added without.
3325 + *
3326 + * @param string $key Extended bucket key
3327 + * @return string|null Label, or null when the bucket has none
3328 + */
3329 + public static function extended_bucket_label(string $key): ?string {
3330 + $labels = [
3331 + 'code_injection' => __('Header, body and footer code', 'thinkrank'),
3332 + 'redirect_settings' => __('Redirect behaviour settings', 'thinkrank'),
3333 + 'taxonomy_settings' => __('Taxonomy templates and images', 'thinkrank'),
3334 + 'post_type_archive_settings' => __('Post type archive templates and images', 'thinkrank'),
3335 + 'no_category_base' => __('Removing /category/ from category URLs', 'thinkrank'),
3336 + 'webmaster_tools' => __('Webmaster tools verification codes', 'thinkrank'),
3337 + 'access_control' => __('Role access settings', 'thinkrank'),
3338 + 'og_frontpage_title' => __('Homepage social title', 'thinkrank'),
3339 + 'og_frontpage_desc' => __('Homepage social description', 'thinkrank'),
3340 + 'og_frontpage_image' => __('Homepage social image', 'thinkrank'),
3341 + 'custom_schemas' => __('Custom schemas (need ThinkRank Pro)', 'thinkrank'),
3342 + 'schema_templates' => __('Schema templates with no ThinkRank equivalent', 'thinkrank'),
3343 + ];
3344 +
3345 + return $labels[$key] ?? null;
3346 + }
3347 +
3348 + /**
3349 + * What is left of a partially-applied extended bucket once the keys
3350 + * migrate_archive_noindex() consumes are removed.
3351 + *
3352 + * `taxonomy_settings` and `post_type_archive_settings` carry a `noindex`
3353 + * per context that is applied, beside templates and images that are not.
3354 + * Any other bucket is returned whole.
3355 + *
3356 + * @param string $key Extended bucket key
3357 + * @param array $value Bucket contents
3358 + * @return array The unapplied part
3359 + */
3360 + private function unapplied_remainder(string $key, array $value): array {
3361 + if ($key !== 'taxonomy_settings' && $key !== 'post_type_archive_settings') {
3362 + return $value;
3363 + }
3364 +
3365 + $remainder = [];
3366 + foreach ($value as $context => $settings) {
3367 + if (!is_array($settings)) {
3368 + continue;
3369 + }
3370 + // Only the blog index has an archive entity to receive a noindex.
3371 + $applied = $key === 'taxonomy_settings' || (string) $context === 'post';
3372 + $rest = $applied ? array_diff_key($settings, ['noindex' => true]) : $settings;
3373 + if (!empty($rest)) {
3374 + $remainder[$context] = $rest;
3375 + }
3376 + }
3377 +
3378 + return $remainder;
3379 + }
3380 +
3381 + /**
3382 + * Apply per-taxonomy and blog-index noindex through the Content Type
3383 + * Matrix (`taxonomy:<slug>` / `archive:blog`), the entities the front end
3384 + * reads robots directives for on those archives.
3385 + *
3386 + * Reads `extended.taxonomy_settings[<taxonomy>].noindex` (Yoast's and
3387 + * Slim SEO's shape) and `extended.post_type_archive_settings.post.noindex`.
3388 + * An entity whose robots were already customised in ThinkRank is left
3389 + * alone, and a source that did NOT noindex an archive writes nothing —
3390 + * "indexed" is the default and restating it would pin the entity.
3391 + *
3392 + * @param array $extended Canonical settings `extended` payload
3393 + * @return bool True if any entity was written
3394 + */
3395 + private function migrate_archive_noindex(array $extended): bool {
3396 + if (!class_exists('ThinkRank\\SEO\\Content_Type_Settings')) {
3397 + return false;
3398 + }
3399 +
3400 + // Only public taxonomies are matrix entities (Content_Type_Settings::get_entities()).
3401 + $entities = [];
3402 + $taxonomies = is_array($extended['taxonomy_settings'] ?? null) ? $extended['taxonomy_settings'] : [];
3403 + foreach ($taxonomies as $taxonomy => $settings) {
3404 + if (!is_array($settings) || empty($settings['noindex']) || !taxonomy_exists((string) $taxonomy)) {
3405 + continue;
3406 + }
3407 + $object = get_taxonomy((string) $taxonomy);
3408 + if (is_object($object) && !empty($object->public)) {
3409 + $entities[] = \ThinkRank\SEO\Content_Type_Settings::PREFIX_TAXONOMY . $taxonomy;
3410 + }
3411 + }
3412 + $archives = is_array($extended['post_type_archive_settings'] ?? null) ? $extended['post_type_archive_settings'] : [];
3413 + if (!empty($archives['post']['noindex'])) {
3414 + $entities[] = \ThinkRank\SEO\Content_Type_Settings::ENTITY_BLOG_INDEX;
3415 + }
3416 +
3417 + $written = false;
3418 + foreach ($entities as $entity_key) {
3419 + $existing = \ThinkRank\SEO\Content_Type_Settings::get_entity_settings($entity_key);
3420 + if (!empty($existing['robots_meta_enabled'])) {
3421 + continue;
3422 + }
3423 + $robots = \ThinkRank\SEO\Content_Type_Settings::default_robots_meta($entity_key);
3424 + $robots['index'] = false;
3425 + $robots['noindex'] = true;
3426 + if (\ThinkRank\SEO\Content_Type_Settings::update_entity_settings($entity_key, [
3427 + 'robots_meta_enabled' => true,
3428 + 'robots_meta' => $robots,
3429 + ])) {
3430 + $written = true;
3431 + }
3432 + }
3433 +
3434 + return $written;
3072 3435 }
3073 3436
3074 3437 /**
3075 3438 * Fold post IDs the source excluded from its sitemap into ThinkRank's