PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.14.2
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.14.2
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 1.28.0 All 57 releases
← All changes | includes/admin/importers/class-snapshot-migrator.php +376 -20 2.11.0 → 2.14.2 View file →
@@ -111,8 +111,15 @@
111 111 'image_seo',
112 112 'sitemap_settings',
113 113 'analytics_connected',
114 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',
115 122 // Capture-all raw buckets (whole source option sets stored verbatim).
116 123 // They live in the SNAPSHOT — cleanup never touches the snapshot — and
117 124 // a re-export recreates them, so they never block cleanup.
118 125 'raw_options',
@@ -374,8 +381,15 @@
374 381 if ($object_type === 'post' && $this->migrate_video_schema($object_id, $data, $record)) {
375 382 $record_had_writes = true;
376 383 }
377 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 +
378 392 // Per-object redirect. SEOPress is the one source that stores a
379 393 // redirect as object meta rather than in a rules table, so its
380 394 // per-post redirects were exported into extended.redirect_* and
381 395 // then dropped for want of anywhere to put them. They have a home
@@ -1765,9 +1779,9 @@
1765 1779 $processed++;
1766 1780 }
1767 1781 }
1768 1782
1769 - 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'])) {
1770 1784 $site_identity = get_option('thinkrank_site_identity_settings', []);
1771 1785 $updated = false;
1772 1786
1773 1787 if (!empty($data['homepage_title']) && empty($site_identity['homepage_title'])) {
@@ -1773,12 +1787,8 @@
1773 1787 if (!empty($data['homepage_title']) && empty($site_identity['homepage_title'])) {
1774 1788 $site_identity['homepage_title'] = $data['homepage_title'];
1775 1789 $updated = true;
1776 1790 }
1777 - if (!empty($data['homepage_description']) && empty($site_identity['homepage_description'])) {
1778 - $site_identity['homepage_description'] = $data['homepage_description'];
1779 - $updated = true;
1780 - }
1781 1791 if (!empty($data['organization_name']) && empty($site_identity['organization_name'])) {
1782 1792 $site_identity['organization_name'] = $data['organization_name'];
1783 1793 $updated = true;
1784 1794 }
@@ -1792,21 +1802,10 @@
1792 1802 $processed++;
1793 1803 }
1794 1804 }
1795 1805
1796 - if (!empty($data['social_profiles'])) {
1797 - $social = get_option('thinkrank_social_media_settings', []);
1798 - $updated = false;
1799 -
1800 - foreach ($data['social_profiles'] as $platform => $url) {
1801 - if (!empty($url) && empty($social[$platform])) {
1802 - $social[$platform] = $url;
1803 - $updated = true;
1804 - }
1805 - }
1806 -
1807 - if ($updated) {
1808 - 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'])) {
1809 1808 $processed++;
1810 1809 }
1811 1810 }
1812 1811
@@ -1863,8 +1862,23 @@
1863 1862 $processed++;
1864 1863 }
1865 1864 }
1866 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 +
1867 1881 // Site-identity settings (homepage/org/breadcrumbs/local SEO) are served to
1868 1882 // the frontend from the wp_thinkrank_seo_settings table via the manager, not
1869 1883 // from the option written above — route them through the manager so they
1870 1884 // actually take effect.
@@ -2000,8 +2014,12 @@
2000 2014 // real template and beats the literal-resolved data.homepage_title, so it
2001 2015 // wins when the source provided one.
2002 2016 $title_formats = is_array($extended['title_formats'] ?? null) ? $extended['title_formats'] : [];
2003 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'] ?? '')));
2004 2022 $set('site_name', $data['organization_name'] ?? '');
2005 2023 $set('alternate_name', $data['alternate_name'] ?? '');
2006 2024 $set('logo_url', $data['organization_logo'] ?? '');
2007 2025
@@ -2043,8 +2061,23 @@
2043 2061 $set('breadcrumb_separator', $breadcrumbs['separator'] ?? '');
2044 2062 $set('breadcrumb_prefix', $breadcrumbs['prefix'] ?? '');
2045 2063 }
2046 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 +
2047 2080 // Local SEO (extended.local_seo) — migrate the full NAP + geo when there
2048 2081 // is any meaningful business data (name, phone, address or coordinates),
2049 2082 // and enable the feature alongside it. Each field is written only while
2050 2083 // ThinkRank's Business Info still holds its default (non-destructive).
@@ -2191,9 +2224,10 @@
2191 2224 }
2192 2225
2193 2226 $app_id = trim((string) ($defaults['facebook_app_id'] ?? ''));
2194 2227 $og_image = trim((string) ($defaults['og_default_image'] ?? ''));
2195 - if ($app_id === '' && $og_image === '') {
2228 + $twitter_image = trim((string) ($defaults['twitter_default_image'] ?? ''));
2229 + if ($app_id === '' && $og_image === '' && $twitter_image === '') {
2196 2230 return false;
2197 2231 }
2198 2232
2199 2233 $manager = new \ThinkRank\SEO\Social_Meta_Manager();
@@ -2209,8 +2243,12 @@
2209 2243 // outside the allow-list never reaches the database.
2210 2244 if ($og_image !== '' && empty($current['default_og_image'])) {
2211 2245 $updates['default_og_image'] = $og_image;
2212 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 + }
2213 2251
2214 2252 if (empty($updates)) {
2215 2253 return false;
2216 2254 }
@@ -2218,8 +2256,97 @@
2218 2256 return (bool) $manager->save_settings('site', null, $updates);
2219 2257 }
2220 2258
2221 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) && \ThinkRank\Core\Url_Validator::is_valid($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 + /**
2222 2349 * Migrate the source plugin's Pinterest site-verification code into
2223 2350 * ThinkRank's core `pinterest_site_verification` setting. Never overwrites
2224 2351 * a configured code.
2225 2352 *
@@ -3066,11 +3193,39 @@
3066 3193 foreach ($extended as $key => $value) {
3067 3194 if (in_array($key, self::HANDLED_EXTENDED_SETTINGS, true) || empty($value)) {
3068 3195 continue;
3069 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 + }
3070 3225 $buckets[] = [
3071 3226 'key' => 'settings.' . $key,
3072 - 'label' => (string) $key,
3227 + 'label' => self::extended_bucket_label((string) $key) ?? ucfirst(str_replace('_', ' ', (string) $key)),
3073 3228 'count' => 1,
3074 3229 ];
3075 3230 }
3076 3231 }
@@ -3075,8 +3230,209 @@
3075 3230 }
3076 3231 }
3077 3232
3078 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;
3079 3435 }
3080 3436
3081 3437 /**
3082 3438 * Fold post IDs the source excluded from its sitemap into ThinkRank's