final class Huttons_News_Importer
{
private const CRON_HOOK = ‘huttons_news_hourly’;
private const CATALOG_URL = ‘https://www.huttonsgroup.com/property-news’;
private const MINIMUM_DATE = ‘2026-01-01’;
private const POST_TYPE = ‘insight’;
private const SOURCE_META = ‘source’;
private const ARTICLE_ID_META = ‘_huttons_article_id’;
private const LOCK_KEY = ‘huttons_news_import_lock’;
public static function init(): void
{
add_action(self::CRON_HOOK, [self::class, ‘run’]);
add_filter(‘cron_schedules’, [self::class, ‘ensure_hourly_schedule’]);
add_action(‘init’, [self::class, ‘ensure_scheduled’], 100);
}
public static function ensure_scheduled(): void
{
if (!wp_next_scheduled(self::CRON_HOOK)) {
wp_schedule_event(time() + 60, ‘hourly’, self::CRON_HOOK);
}
}
public static function ensure_hourly_schedule(array $schedules): array
{
if (!isset($schedules[‘hourly’])) {
$schedules[‘hourly’] = [
‘interval’ => HOUR_IN_SECONDS,
‘display’ => __(‘Once Hourly’, ‘huttons-news-importer’),
];
}
return $schedules;
}
public static function run()
{
if (get_transient(self::LOCK_KEY)) {
return new WP_Error(‘huttons_import_locked’, ‘A Huttons import is already running.’);
}
set_transient(self::LOCK_KEY, 1, 15 * MINUTE_IN_SECONDS);
try {
if (!post_type_exists(self::POST_TYPE)) {
return new WP_Error(‘huttons_missing_post_type’, ‘The insight post type does not exist.’);
}
$articles = self::fetch_catalog();
if (is_wp_error($articles)) {
return $articles;
}
foreach ($articles as $article) {
if (self::already_imported($article)) {
continue;
}
return self::import_article($article);
}
return 0;
} finally {
delete_transient(self::LOCK_KEY);
}
}
private static function fetch_catalog()
{
$response = wp_remote_get(self::CATALOG_URL, self::request_args());
if (is_wp_error($response)) {
return $response;
}
$status = wp_remote_retrieve_response_code($response);
if ($status !== 200) {
return new WP_Error(‘huttons_catalog_http_error’, ‘Huttons catalog returned HTTP ‘ . $status . ‘.’);
}
$payload = self::extract_inertia_payload(wp_remote_retrieve_body($response));
if (is_wp_error($payload)) {
return $payload;
}
$collections = [];
self::find_article_collections($payload, $collections);
if (!$collections) {
return new WP_Error(‘huttons_no_articles’, ‘No articles were found in the Huttons page data.’);
}
usort($collections, static function (array $a, array $b): int {
return count($b) <=> count($a);
});
$articles = [];
foreach ($collections[0] as $raw) {
$url = !empty($raw[‘link’]) ? esc_url_raw($raw[‘link’]) : ”;
if (!$url && !empty($raw[‘slug’])) {
$url = ‘https://www.huttonsgroup.com/article/’ . sanitize_title($raw[‘slug’]);
}
if (!$url || empty($raw[‘title’])) {
continue;
}
$published_date = self::normalise_date($raw[‘date’] ?? ”);
if (!$published_date || $published_date < self::MINIMUM_DATE) {
continue;
}
$articles[] = [
'id' => isset($raw[‘id’]) ? absint($raw[‘id’]) : 0,
‘title’ => html_entity_decode(wp_strip_all_tags($raw[‘title’]), ENT_QUOTES | ENT_HTML5, ‘UTF-8’),
‘date’ => $published_date,
‘url’ => $url,
‘image’ => !empty($raw[‘image’]) ? esc_url_raw($raw[‘image’]) : ”,
‘source’ => !empty($raw[‘source’]) ? sanitize_text_field($raw[‘source’]) : ‘Huttons Group’,
‘category’ => !empty($raw[‘category’]) ? sanitize_text_field($raw[‘category’]) : ”,
‘slug’ => !empty($raw[‘slug’]) ? sanitize_title($raw[‘slug’]) : sanitize_title($raw[‘title’]),
];
}
usort($articles, static function (array $a, array $b): int {
return strcmp($b[‘date’], $a[‘date’]);
});
return $articles;
}
private static function import_article(array $article)
{
$body = self::fetch_article_body($article[‘url’]);
if (is_wp_error($body)) {
return $body;
}
$body = self::remove_content_after_end_marker($body);
$source_link = sprintf(
‘
Source: Huttons Group
‘,
esc_url($article[‘url’])
);
$post_date = $article[‘date’] ? $article[‘date’] . ‘ 12:00:00’ : current_time(‘mysql’);
$post_id = wp_insert_post([
‘post_type’ => self::POST_TYPE,
‘post_status’ => ‘publish’,
‘post_title’ => $article[‘title’],
‘post_name’ => $article[‘slug’],
‘post_content’ => wp_kses_post($body . “\n” . $source_link),
‘post_excerpt’ => ”,
‘post_date’ => $post_date,
], true);
if (is_wp_error($post_id)) {
return $post_id;
}
update_post_meta($post_id, self::SOURCE_META, $article[‘url’]);
if ($article[‘id’]) {
update_post_meta($post_id, self::ARTICLE_ID_META, $article[‘id’]);
}
if (!empty($article[‘image’])) {
self::set_featured_image($post_id, $article[‘image’], $article[‘title’]);
}
return $post_id;
}
private static function remove_content_after_end_marker(string $content): string
{
$position = strpos($content, ‘- END -‘);
if ($position !== false) {
$block_start = false;
foreach ([‘
$block_start)) {
$block_start = $candidate;
}
}
$content = substr($content, 0, $block_start !== false ? $block_start : $position);
}
return force_balance_tags(trim($content));
}
private static function set_featured_image(int $post_id, string $image_url, string $title): void
{
require_once ABSPATH . ‘wp-admin/includes/file.php’;
require_once ABSPATH . ‘wp-admin/includes/media.php’;
require_once ABSPATH . ‘wp-admin/includes/image.php’;
if (strpos($image_url, ‘//’) === 0) {
$image_url = ‘https:’ . $image_url;
} elseif (strpos($image_url, ‘/’) === 0) {
$image_url = ‘https://www.huttonsgroup.com’ . $image_url;
}
$temporary_file = download_url($image_url, 30);
if (is_wp_error($temporary_file)) {
return;
}
$path = (string) wp_parse_url($image_url, PHP_URL_PATH);
$filename = sanitize_file_name(wp_basename($path));
if (!$filename || !pathinfo($filename, PATHINFO_EXTENSION)) {
$filename = sanitize_title($title) . ‘.jpg’;
}
$file = [
‘name’ => $filename,
‘tmp_name’ => $temporary_file,
];
$attachment_id = media_handle_sideload($file, $post_id, $title);
if (is_wp_error($attachment_id)) {
@unlink($temporary_file);
return;
}
set_post_thumbnail($post_id, $attachment_id);
update_post_meta($attachment_id, ‘_wp_attachment_image_alt’, sanitize_text_field($title));
}
private static function fetch_article_body(string $url)
{
$response = wp_remote_get($url, self::request_args());
if (is_wp_error($response)) {
return $response;
}
$status = wp_remote_retrieve_response_code($response);
if ($status !== 200) {
return new WP_Error(‘huttons_article_http_error’, ‘Huttons article returned HTTP ‘ . $status . ‘.’);
}
$html = wp_remote_retrieve_body($response);
$payload = self::extract_inertia_payload($html);
if (!is_wp_error($payload)) {
$content = self::find_largest_content($payload);
if ($content) {
return wp_kses_post($content);
}
}
if (class_exists(‘DOMDocument’)) {
$document = new DOMDocument();
libxml_use_internal_errors(true);
$loaded = $document->loadHTML(‘‘ . $html);
libxml_clear_errors();
if ($loaded) {
$xpath = new DOMXPath($document);
foreach ([‘//article’, ‘//main’] as $query) {
$nodes = $xpath->query($query);
if ($nodes && $nodes->length) {
return wp_kses_post(self::inner_html($nodes->item(0)));
}
}
}
}
return ‘
Read the full article at Huttons Group.
‘;
}
private static function extract_inertia_payload(string $html)
{
if (!preg_match(‘/data-page=(?:”([^”]+)”|\'([^\’]+)\’)/s’, $html, $matches)) {
return new WP_Error(‘huttons_missing_payload’, ‘The Huttons Inertia page payload was not found.’);
}
$encoded = $matches[1] !== ” ? $matches[1] : $matches[2];
$decoded = html_entity_decode($encoded, ENT_QUOTES | ENT_HTML5, ‘UTF-8’);
$payload = json_decode($decoded, true);
return is_array($payload)
? $payload
: new WP_Error(‘huttons_invalid_payload’, ‘The Huttons page payload was invalid JSON.’);
}
private static function find_article_collections($value, array &$collections): void
{
if (!is_array($value)) {
return;
}
if (self::is_list($value)) {
$matches = array_values(array_filter($value, static function ($item): bool {
return is_array($item) && !empty($item[‘title’]) && (!empty($item[‘link’]) || !empty($item[‘slug’]));
}));
if ($matches && count($matches) >= max(1, intdiv(count($value), 2))) {
$collections[] = $matches;
return;
}
}
foreach ($value as $child) {
self::find_article_collections($child, $collections);
}
}
private static function find_largest_content($value): string
{
$candidates = [];
self::collect_content($value, $candidates);
if (!$candidates) {
return ”;
}
usort($candidates, static function (string $a, string $b): int {
return strlen($b) <=> strlen($a);
});
return $candidates[0];
}
private static function collect_content($value, array &$candidates): void
{
if (!is_array($value)) {
return;
}
foreach ($value as $key => $child) {
if (is_string($child) && in_array((string) $key, [‘content’, ‘body’, ‘article_content’], true) && strlen(wp_strip_all_tags($child)) > 100) {
$candidates[] = $child;
} elseif (is_array($child)) {
self::collect_content($child, $candidates);
}
}
}
private static function already_imported(array $article): bool
{
$existing = get_posts([
‘post_type’ => self::POST_TYPE,
‘post_status’ => ‘any’,
‘posts_per_page’ => 1,
‘fields’ => ‘ids’,
‘meta_key’ => self::SOURCE_META,
‘meta_value’ => $article[‘url’],
‘no_found_rows’ => true,
]);
if ($existing) {
return true;
}
return (bool) get_page_by_path($article[‘slug’], OBJECT, self::POST_TYPE);
}
private static function normalise_date(string $date): string
{
$parsed = DateTime::createFromFormat(‘d/m/Y’, $date);
return $parsed ? $parsed->format(‘Y-m-d’) : ”;
}
private static function is_list(array $value): bool
{
if ($value === []) {
return true;
}
return array_keys($value) === range(0, count($value) – 1);
}
private static function request_args(): array
{
return [
‘timeout’ => 30,
‘redirection’ => 5,
‘user-agent’ => ‘EzyHomes-Huttons-News-Importer/1.0; ‘ . home_url(‘/’),
‘headers’ => [‘Accept’ => ‘text/html,application/xhtml+xml’],
];
}
private static function inner_html(DOMNode $node): string
{
$html = ”;
foreach ($node->childNodes as $child) {
$html .= $node->ownerDocument->saveHTML($child);
}
return $html;
}
}
Huttons_News_Importer::init();
if (defined(‘WP_CLI’) && WP_CLI) {
WP_CLI::add_command(‘huttons-news import-one’, static function (): void {
$result = Huttons_News_Importer::run();
if (is_wp_error($result)) {
WP_CLI::error($result->get_error_message());
} elseif ($result === 0) {
WP_CLI::success(‘No unimported Huttons articles remain.’);
} else {
WP_CLI::success(‘Imported insight post #’ . $result . ‘.’);
}
});
}